diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index 6afa0bfcbdb6d..eba2e7874574d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -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 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(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index 95e3444f47797..09f163810d27f 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -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