Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 436b9fd2 authored by Josh Tsuji's avatar Josh Tsuji Committed by Android (Google) Code Review
Browse files

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.
parents 41d2cdf3 2e25246c
Loading
Loading
Loading
Loading
+162 −137
Original line number Diff line number Diff line
@@ -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);
    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();
    }
            mBoundsAnimator.start();

    /**
     * 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();
    }

    /**
@@ -378,64 +425,42 @@ public class PipMotionHelper implements Handler.Callback, PipAppOpsListener.Call
    }

    /**
     * Animates the dismissal of the PiP off the edge of the screen.
     * Cancels all existing animations.
     */
    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;
    private void cancelAnimations() {
        mAnimatedBoundsPhysicsAnimator.cancel();
        mVsyncTimeAnimator.cancel();
    }

    /**
     * Cancels all existing animations.
     * Set new fling configs whose min/max values respect the given movement bounds, and set the
     * animated bounds to PIP's current 'real' bounds.
     */
    void cancelAnimations() {
        if (mBoundsAnimator != null) {
            mBoundsAnimator.cancel();
            mBoundsAnimator = null;
        }
    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);
    }

    /**
     * Creates an animation to move the PiP to give given {@param toBounds}.
     * 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 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 startBoundsAnimation() {
        cancelAnimations();

        mAnimatedBoundsPhysicsAnimator
                .withEndActions(
                        mVsyncTimeAnimator::cancel)
                .start();
        mVsyncTimeAnimator.start();
    }

    /**
+15 −39
Original line number Diff line number Diff line
@@ -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
+107 −0

File added.

Preview size limit exceeded, changes collapsed.

+27 −12
Original line number Diff line number Diff line
@@ -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
            // 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()
                }