Merge changes I90625ae0,I96aaa686,I0f6584e1,I75219370,I68ed3bee

* changes:
  Modifies the PipMotionHelper to use PhysicsAnimator for PIP motion.
  Fix issue with left-flings when distanceToDestination = 0.
  Add FloatProperties, which contains helpful properties for animating Rects.
  Filter end actions for null, and allow Runnables from Java
  Cancel flings before restarting them.
This commit is contained in:
Josh Tsuji
2020-01-21 19:58:40 +00:00
committed by Android (Google) Code Review
4 changed files with 314 additions and 191 deletions

View File

@@ -19,17 +19,11 @@ package com.android.systemui.pip.phone;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN;
import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.animation.TimeAnimator;
import android.annotation.Nullable;
import android.app.ActivityManager.StackInfo;
import android.app.IActivityManager;
import android.app.IActivityTaskManager;
@@ -42,13 +36,16 @@ import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.animation.Interpolator;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.internal.os.SomeArgs;
import com.android.systemui.pip.PipSnapAlgorithm;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.util.animation.FloatProperties;
import com.android.systemui.util.animation.PhysicsAnimator;
import java.io.PrintWriter;
@@ -60,18 +57,14 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
private static final String TAG = "PipMotionHelper";
private static final boolean DEBUG = false;
private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
private static final int DEFAULT_MOVE_STACK_DURATION = 225;
private static final int SNAP_STACK_DURATION = 225;
private static final int DRAG_TO_TARGET_DISMISS_STACK_DURATION = 375;
private static final int DRAG_TO_DISMISS_STACK_DURATION = 175;
private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
private static final int EXPAND_STACK_TO_FULLSCREEN_DURATION = 300;
private static final int MINIMIZE_STACK_MAX_DURATION = 200;
private static final int SHIFT_DURATION = 300;
/** Friction to use for PIP when it moves via physics fling animations. */
private static final float DEFAULT_FRICTION = 2f;
// The fraction of the stack width that the user has to drag offscreen to minimize the PiP
private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.3f;
// The fraction of the stack height that the user has to drag offscreen to dismiss the PiP
@@ -89,12 +82,39 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
private PipMenuActivityController mMenuController;
private PipSnapAlgorithm mSnapAlgorithm;
private FlingAnimationUtils mFlingAnimationUtils;
private AnimationHandler mAnimationHandler;
private final Rect mBounds = new Rect();
private final Rect mStableInsets = new Rect();
private ValueAnimator mBoundsAnimator = null;
/** PIP's current bounds on the screen. */
private final Rect mBounds = new Rect();
/**
* Bounds that are animated using the physics animator. PIP is moved to these bounds whenever
* the {@link #mVsyncTimeAnimator} ticks.
*/
private final Rect mAnimatedBounds = new Rect();
/**
* PhysicsAnimator instance for animating {@link #mAnimatedBounds} using physics animations.
*/
private PhysicsAnimator<Rect> mAnimatedBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
mAnimatedBounds);
/**
* Time animator whose frame timing comes from the SurfaceFlinger vsync frame provider. At each
* frame, PIP is moved to {@link #mAnimatedBounds}, which are animated asynchronously using
* physics animations.
*/
private TimeAnimator mVsyncTimeAnimator;
/** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
private PhysicsAnimator.FlingConfig mFlingConfigX;
private PhysicsAnimator.FlingConfig mFlingConfigY;
/** SpringConfig to use for fling-then-spring animations. */
private final PhysicsAnimator.SpringConfig mSpringConfig =
new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
public PipMotionHelper(Context context, IActivityManager activityManager,
IActivityTaskManager activityTaskManager, PipMenuActivityController menuController,
@@ -106,9 +126,39 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
mMenuController = menuController;
mSnapAlgorithm = snapAlgorithm;
mFlingAnimationUtils = flingAnimationUtils;
mAnimationHandler = new AnimationHandler();
mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider());
final AnimationHandler vsyncFrameCallbackProvider = new AnimationHandler();
vsyncFrameCallbackProvider.setProvider(new SfVsyncFrameCallbackProvider());
onConfigurationChanged();
// Construct a time animator that uses the vsync frame provider. Physics animations can't
// use custom frame providers, since they rely on constant time between frames to run the
// physics simulations. To work around this, we physically-animate a second set of bounds,
// and apply those animating bounds to the PIP in-sync via this TimeAnimator.
mVsyncTimeAnimator = new TimeAnimator() {
@Override
public AnimationHandler getAnimationHandler() {
return vsyncFrameCallbackProvider;
}
};
// When the time animator ticks, move PIP to the animated bounds.
mVsyncTimeAnimator.setTimeListener(
(animation, totalTime, deltaTime) ->
resizePipUnchecked(mAnimatedBounds));
// Add a listener for cancel/end events that moves PIP to the final animated bounds.
mVsyncTimeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
resizePipUnchecked(mAnimatedBounds);
}
@Override
public void onAnimationEnd(Animator animation) {
resizePipUnchecked(mAnimatedBounds);
}
});
}
/**
@@ -240,89 +290,86 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
return false;
}
/**
* Flings the minimized PiP to the closest minimized snap target.
*/
Rect flingToMinimizedState(float velocityY, Rect movementBounds, Point dragStartPosition) {
cancelAnimations();
// We currently only allow flinging the minimized stack up and down, so just lock the
// movement bounds to the current stack bounds horizontally
movementBounds = new Rect(mBounds.left, movementBounds.top, mBounds.left,
movementBounds.bottom);
Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds,
0 /* velocityX */, velocityY, dragStartPosition);
if (!mBounds.equals(toBounds)) {
mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN);
mFlingAnimationUtils.apply(mBoundsAnimator, 0,
distanceBetweenRectOffsets(mBounds, toBounds),
velocityY);
mBoundsAnimator.start();
}
return toBounds;
}
/**
* Animates the PiP to the minimized state, slightly offscreen.
*/
Rect animateToClosestMinimizedState(Rect movementBounds,
AnimatorUpdateListener updateListener) {
cancelAnimations();
Rect toBounds = getClosestMinimizedBounds(mBounds, movementBounds);
if (!mBounds.equals(toBounds)) {
mBoundsAnimator = createAnimationToBounds(mBounds, toBounds,
MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN);
if (updateListener != null) {
mBoundsAnimator.addUpdateListener(updateListener);
}
mBoundsAnimator.start();
void animateToClosestMinimizedState(Rect movementBounds, @Nullable Runnable updateAction) {
final Rect toBounds = getClosestMinimizedBounds(mBounds, movementBounds);
prepareForBoundsAnimation(movementBounds);
mAnimatedBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
.spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig);
if (updateAction != null) {
mAnimatedBoundsPhysicsAnimator.addUpdateListener(
(target, values) -> updateAction.run());
}
return toBounds;
startBoundsAnimation();
}
/**
* Flings the PiP to the closest snap target.
*/
Rect flingToSnapTarget(float velocity, float velocityX, float velocityY, Rect movementBounds,
AnimatorUpdateListener updateListener, AnimatorListener listener,
Point startPosition) {
cancelAnimations();
Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds,
velocityX, velocityY, startPosition);
if (!mBounds.equals(toBounds)) {
mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN);
mFlingAnimationUtils.apply(mBoundsAnimator, 0,
distanceBetweenRectOffsets(mBounds, toBounds),
velocity);
if (updateListener != null) {
mBoundsAnimator.addUpdateListener(updateListener);
}
if (listener != null){
mBoundsAnimator.addListener(listener);
}
mBoundsAnimator.start();
}
return toBounds;
void flingToSnapTarget(
float velocityX, float velocityY, Rect movementBounds, Runnable updateAction,
@Nullable Runnable endAction) {
prepareForBoundsAnimation(movementBounds);
mAnimatedBoundsPhysicsAnimator
.flingThenSpring(
FloatProperties.RECT_X, velocityX, mFlingConfigX, mSpringConfig,
true /* flingMustReachMinOrMax */)
.flingThenSpring(
FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig)
.addUpdateListener((target, values) -> updateAction.run())
.withEndActions(endAction);
startBoundsAnimation();
}
/**
* Animates the PiP to the closest snap target.
*/
Rect animateToClosestSnapTarget(Rect movementBounds, AnimatorUpdateListener updateListener,
AnimatorListener listener) {
cancelAnimations();
Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds);
if (!mBounds.equals(toBounds)) {
mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, SNAP_STACK_DURATION,
FAST_OUT_SLOW_IN);
if (updateListener != null) {
mBoundsAnimator.addUpdateListener(updateListener);
}
if (listener != null){
mBoundsAnimator.addListener(listener);
}
mBoundsAnimator.start();
void animateToClosestSnapTarget(Rect movementBounds) {
prepareForBoundsAnimation(movementBounds);
final Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds);
mAnimatedBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
.spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig);
startBoundsAnimation();
}
/**
* Animates the dismissal of the PiP off the edge of the screen.
*/
void animateDismiss(float velocityX, float velocityY, @Nullable Runnable updateAction) {
final float velocity = PointF.length(velocityX, velocityY);
final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
final Point dismissEndPoint = getDismissEndPoint(mBounds, velocityX, velocityY, isFling);
// Set the animated bounds to start at the current bounds. We don't need to rebuild the
// fling configs here via prepareForBoundsAnimation, since animateDismiss isn't provided
// with new movement bounds.
mAnimatedBounds.set(mBounds);
// Animate to the dismiss end point, and then dismiss PIP.
mAnimatedBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, dismissEndPoint.x, velocityX, mSpringConfig)
.spring(FloatProperties.RECT_Y, dismissEndPoint.y, velocityY, mSpringConfig)
.withEndActions(this::dismissPip);
// If we were provided with an update action, run it whenever there's an update.
if (updateAction != null) {
mAnimatedBoundsPhysicsAnimator.addUpdateListener(
(target, values) -> updateAction.run());
}
return toBounds;
startBoundsAnimation();
}
/**
@@ -377,65 +424,43 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
mHandler.sendMessage(mHandler.obtainMessage(MSG_OFFSET_ANIMATE, args));
}
/**
* Animates the dismissal of the PiP off the edge of the screen.
*/
Rect animateDismiss(Rect pipBounds, float velocityX, float velocityY,
AnimatorUpdateListener listener) {
cancelAnimations();
final float velocity = PointF.length(velocityX, velocityY);
final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
Point p = getDismissEndPoint(pipBounds, velocityX, velocityY, isFling);
Rect toBounds = new Rect(pipBounds);
toBounds.offsetTo(p.x, p.y);
mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, DRAG_TO_DISMISS_STACK_DURATION,
FAST_OUT_LINEAR_IN);
mBoundsAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dismissPip();
}
});
if (isFling) {
mFlingAnimationUtils.apply(mBoundsAnimator, 0,
distanceBetweenRectOffsets(mBounds, toBounds), velocity);
}
if (listener != null) {
mBoundsAnimator.addUpdateListener(listener);
}
mBoundsAnimator.start();
return toBounds;
}
/**
* Cancels all existing animations.
*/
void cancelAnimations() {
if (mBoundsAnimator != null) {
mBoundsAnimator.cancel();
mBoundsAnimator = null;
}
private void cancelAnimations() {
mAnimatedBoundsPhysicsAnimator.cancel();
mVsyncTimeAnimator.cancel();
}
/**
* Creates an animation to move the PiP to give given {@param toBounds}.
* Set new fling configs whose min/max values respect the given movement bounds, and set the
* animated bounds to PIP's current 'real' bounds.
*/
private ValueAnimator createAnimationToBounds(Rect fromBounds, Rect toBounds, int duration,
Interpolator interpolator) {
ValueAnimator anim = new ValueAnimator() {
@Override
public AnimationHandler getAnimationHandler() {
return mAnimationHandler;
}
};
anim.setObjectValues(fromBounds, toBounds);
anim.setEvaluator(RECT_EVALUATOR);
anim.setDuration(duration);
anim.setInterpolator(interpolator);
anim.addUpdateListener((ValueAnimator animation) -> {
resizePipUnchecked((Rect) animation.getAnimatedValue());
});
return anim;
private void prepareForBoundsAnimation(Rect movementBounds) {
mFlingConfigX = new PhysicsAnimator.FlingConfig(
DEFAULT_FRICTION, movementBounds.left, movementBounds.right);
mFlingConfigY = new PhysicsAnimator.FlingConfig(
DEFAULT_FRICTION, movementBounds.top, movementBounds.bottom);
mAnimatedBounds.set(mBounds);
}
/**
* Starts the physics animator which will update the animated PIP bounds using physics
* animations, as well as the TimeAnimator which will apply those bounds to PIP at intervals
* synchronized with the SurfaceFlinger vsync frame provider.
*
* This will also add end actions to the bounds animator that cancel the TimeAnimator and update
* the 'real' bounds to equal the final animated bounds.
*/
private void startBoundsAnimation() {
cancelAnimations();
mAnimatedBoundsPhysicsAnimator
.withEndActions(
mVsyncTimeAnimator::cancel)
.start();
mVsyncTimeAnimator.start();
}
/**

View File

@@ -20,10 +20,6 @@ import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STAT
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.IActivityManager;
import android.app.IActivityTaskManager;
import android.content.ComponentName;
@@ -111,13 +107,6 @@ public class PipTouchHandler {
}
}
};
private ValueAnimator.AnimatorUpdateListener mUpdateScrimListener =
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateDismissFraction();
}
};
// Behaviour states
private int mMenuState = MENU_STATE_NONE;
@@ -162,7 +151,7 @@ public class PipTouchHandler {
@Override
public void onPipMinimize() {
setMinimizedStateInternal(true);
mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateListener */);
mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateAction */);
}
@Override
@@ -655,15 +644,6 @@ public class PipTouchHandler {
float lastY = mStartPosition.y + mDelta.y;
float left = lastX + lastDelta.x;
float top = lastY + lastDelta.y;
if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) {
left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left));
}
if (mEnableDimissDragToEdge) {
// Allow pip to move past bottom bounds
top = Math.max(mMovementBounds.top, top);
} else {
top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top));
}
// Add to the cumulative delta after bounding the position
mDelta.x += left - lastX;
@@ -720,8 +700,9 @@ public class PipTouchHandler {
if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
PipUtils.getTopPinnedActivity(mContext, mActivityManager));
mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x,
vel.y, mUpdateScrimListener);
mMotionHelper.animateDismiss(
vel.x, vel.y,
PipTouchHandler.this::updateDismissFraction /* updateAction */);
return true;
}
}
@@ -739,8 +720,9 @@ public class PipTouchHandler {
// minimize offset adjusted
mMenuController.hideMenu();
} else {
mMotionHelper.animateToClosestMinimizedState(mMovementBounds,
mUpdateScrimListener);
mMotionHelper.animateToClosestMinimizedState(
mMovementBounds,
PipTouchHandler.this::updateDismissFraction /* updateAction */);
}
return true;
}
@@ -750,7 +732,7 @@ public class PipTouchHandler {
setMinimizedStateInternal(false);
}
AnimatorListenerAdapter postAnimationCallback = null;
Runnable endAction = null;
if (mMenuState != MENU_STATE_NONE) {
// If the menu is still visible, and we aren't minimized, then just poke the
// menu so that it will timeout after the user stops touching it
@@ -759,26 +741,20 @@ public class PipTouchHandler {
} else {
// If the menu is not visible, then we can still be showing the activity for the
// dismiss overlay, so just finish it after the animation completes
postAnimationCallback = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mMenuController.hideMenu();
}
};
endAction = mMenuController::hideMenu;
}
if (isFling) {
mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds,
mUpdateScrimListener, postAnimationCallback,
mStartPosition);
mMotionHelper.flingToSnapTarget(
vel.x, vel.y, mMovementBounds,
PipTouchHandler.this::updateDismissFraction /* updateAction */,
endAction /* endAction */);
} else {
mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener,
postAnimationCallback);
mMotionHelper.animateToClosestSnapTarget(mMovementBounds);
}
} else if (mIsMinimized) {
// This was a tap, so no longer minimized
mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */,
null /* animatorListener */);
mMotionHelper.animateToClosestSnapTarget(mMovementBounds);
setMinimizedStateInternal(false);
} else if (mTouchState.isDoubleTap()) {
// Expand to fullscreen if this is a double tap

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.animation
import android.graphics.Rect
import android.graphics.RectF
import androidx.dynamicanimation.animation.FloatPropertyCompat
/**
* Helpful extra properties to use with the [PhysicsAnimator]. These allow you to animate objects
* such as [Rect] and [RectF].
*
* There are additional, more basic properties available in [DynamicAnimation].
*/
class FloatProperties {
companion object {
/**
* Represents the x-coordinate of a [Rect]. Typically used to animate moving a Rect
* horizontally.
*
* This property's getter returns [Rect.left], and its setter uses [Rect.offsetTo], which
* sets [Rect.left] to the new value and offsets [Rect.right] so that the width of the Rect
* does not change.
*/
@JvmField
val RECT_X = object : FloatPropertyCompat<Rect>("RectX") {
override fun setValue(rect: Rect?, value: Float) {
rect?.offsetTo(value.toInt(), rect.top)
}
override fun getValue(rect: Rect?): Float {
return rect?.left?.toFloat() ?: -Float.MAX_VALUE
}
}
/**
* Represents the y-coordinate of a [Rect]. Typically used to animate moving a Rect
* vertically.
*
* This property's getter returns [Rect.top], and its setter uses [Rect.offsetTo], which
* sets [Rect.top] to the new value and offsets [Rect.bottom] so that the height of the Rect
* does not change.
*/
@JvmField
val RECT_Y = object : FloatPropertyCompat<Rect>("RectY") {
override fun setValue(rect: Rect?, value: Float) {
rect?.offsetTo(rect.left, value.toInt())
}
override fun getValue(rect: Rect?): Float {
return rect?.top?.toFloat() ?: -Float.MAX_VALUE
}
}
/**
* Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF
* horizontally.
*
* This property's getter returns [RectF.left], and its setter uses [RectF.offsetTo], which
* sets [RectF.left] to the new value and offsets [RectF.right] so that the width of the
* RectF does not change.
*/
@JvmField
val RECTF_X = object : FloatPropertyCompat<RectF>("RectFX") {
override fun setValue(rect: RectF?, value: Float) {
rect?.offsetTo(value, rect.top)
}
override fun getValue(rect: RectF?): Float {
return rect?.left ?: -Float.MAX_VALUE
}
}
/**
* Represents the y-coordinate of a [RectF]. Typically used to animate moving a RectF
* vertically.
*
* This property's getter returns [RectF.top], and its setter uses [RectF.offsetTo], which
* sets [RectF.top] to the new value and offsets [RectF.bottom] so that the height of the
* RectF does not change.
*/
@JvmField
val RECTF_Y = object : FloatPropertyCompat<RectF>("RectFY") {
override fun setValue(rect: RectF?, value: Float) {
rect?.offsetTo(rect.left, value)
}
override fun getValue(rect: RectF?): Float {
return rect?.top ?: -Float.MAX_VALUE
}
}
}
}

View File

@@ -293,15 +293,19 @@ class PhysicsAnimator<T> private constructor (val target: T) {
val velocityToReachDestination = distanceToDestination *
(flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER)
// Try to use the provided start velocity, but use the required velocity to reach the
// destination if the provided velocity is insufficient.
val sufficientVelocity =
if (distanceToDestination < 0)
min(velocityToReachDestination, startVelocity)
else
max(velocityToReachDestination, startVelocity)
// If there's distance to cover, and the provided velocity is moving in the correct
// direction, ensure that the velocity is high enough to reach the destination.
// Otherwise, just use startVelocity - this means that the fling is at or out of bounds.
// The fling will immediately end and a spring will bring the object back into bounds
// with this startVelocity.
flingConfigCopy.startVelocity = when {
distanceToDestination > 0f && startVelocity >= 0f ->
max(velocityToReachDestination, startVelocity)
distanceToDestination < 0f && startVelocity <= 0f ->
min(velocityToReachDestination, startVelocity)
else -> startVelocity
}
flingConfigCopy.startVelocity = sufficientVelocity
springConfigCopy.finalPosition = toAtLeast
} else {
flingConfigCopy.startVelocity = startVelocity
@@ -367,8 +371,17 @@ class PhysicsAnimator<T> private constructor (val target: T) {
* animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param,
* which indicates that all relevant animations have ended.
*/
fun withEndActions(vararg endActions: EndAction): PhysicsAnimator<T> {
this.endActions.addAll(endActions)
fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> {
this.endActions.addAll(endActions.filterNotNull())
return this
}
/**
* Helper overload so that callers from Java can use Runnables or method references as end
* actions without having to explicitly return Unit.
*/
fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator<T> {
this.endActions.addAll(endActions.filterNotNull().map { it::run })
return this
}
@@ -416,8 +429,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
max = max(currentValue, this.max)
}
// Apply the configuration and start the animation.
// Apply the configuration and start the animation. Since flings can't be
// redirected while in motion, cancel it first.
getFlingAnimation(animatedProperty)
.also { it.cancel() }
.also { flingConfig.applyToAnimation(it) }
.start()
}