| /* |
| * 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.wm.shell.pip.phone; |
| |
| import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY; |
| import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; |
| import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; |
| |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; |
| import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT; |
| import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; |
| import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; |
| import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_DISMISS; |
| import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.os.Debug; |
| import android.os.Looper; |
| import android.util.Log; |
| import android.view.Choreographer; |
| |
| import androidx.dynamicanimation.animation.AnimationHandler; |
| import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; |
| |
| import com.android.wm.shell.R; |
| import com.android.wm.shell.animation.FloatProperties; |
| import com.android.wm.shell.animation.PhysicsAnimator; |
| import com.android.wm.shell.common.FloatingContentCoordinator; |
| import com.android.wm.shell.common.magnetictarget.MagnetizedObject; |
| import com.android.wm.shell.pip.PipBoundsState; |
| import com.android.wm.shell.pip.PipSnapAlgorithm; |
| import com.android.wm.shell.pip.PipTaskOrganizer; |
| import com.android.wm.shell.pip.PipTransitionController; |
| |
| import java.util.function.Consumer; |
| |
| import kotlin.Unit; |
| import kotlin.jvm.functions.Function0; |
| |
| /** |
| * A helper to animate and manipulate the PiP. |
| */ |
| public class PipMotionHelper implements PipAppOpsListener.Callback, |
| FloatingContentCoordinator.FloatingContent { |
| |
| private static final String TAG = "PipMotionHelper"; |
| private static final boolean DEBUG = false; |
| |
| private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; |
| private static final int EXPAND_STACK_TO_MENU_DURATION = 250; |
| private static final int UNSTASH_DURATION = 250; |
| private static final int LEAVE_PIP_DURATION = 300; |
| 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 = 1.9f; |
| /** How much of the dismiss circle size to use when scaling down PIP. **/ |
| private static final float DISMISS_CIRCLE_PERCENT = 0.85f; |
| |
| private final Context mContext; |
| private final PipTaskOrganizer mPipTaskOrganizer; |
| private @NonNull PipBoundsState mPipBoundsState; |
| |
| private PhonePipMenuController mMenuController; |
| private PipSnapAlgorithm mSnapAlgorithm; |
| |
| /** The region that all of PIP must stay within. */ |
| private final Rect mFloatingAllowedArea = new Rect(); |
| |
| /** Coordinator instance for resolving conflicts with other floating content. */ |
| private FloatingContentCoordinator mFloatingContentCoordinator; |
| |
| private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = |
| ThreadLocal.withInitial(() -> { |
| final Looper initialLooper = Looper.myLooper(); |
| final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() { |
| @Override |
| public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) { |
| Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); |
| } |
| |
| @Override |
| public boolean isCurrentThread() { |
| return Looper.myLooper() == initialLooper; |
| } |
| }; |
| AnimationHandler handler = new AnimationHandler(scheduler); |
| return handler; |
| }); |
| |
| /** |
| * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} |
| * using physics animations. |
| */ |
| private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator; |
| |
| private MagnetizedObject<Rect> mMagnetizedPip; |
| |
| /** |
| * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}. |
| */ |
| private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; |
| |
| /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ |
| private PhysicsAnimator.FlingConfig mFlingConfigX; |
| private PhysicsAnimator.FlingConfig mFlingConfigY; |
| /** FlingConfig instances proviced to PhysicsAnimator for stashing. */ |
| private PhysicsAnimator.FlingConfig mStashConfigX; |
| |
| /** SpringConfig to use for fling-then-spring animations. */ |
| private final PhysicsAnimator.SpringConfig mSpringConfig = |
| new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY); |
| |
| /** SpringConfig used for animating into the dismiss region, matches the one in |
| * {@link MagnetizedObject}. */ |
| private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig = |
| new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY); |
| |
| /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss |
| * drag region. */ |
| private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig = |
| new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY); |
| |
| /** SpringConfig to use for springing PIP away from conflicting floating content. */ |
| private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = |
| new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); |
| |
| private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { |
| if (mPipBoundsState.getBounds().equals(newBounds)) { |
| return; |
| } |
| |
| mMenuController.updateMenuLayout(newBounds); |
| mPipBoundsState.setBounds(newBounds); |
| }; |
| |
| /** |
| * Whether we're springing to the touch event location (vs. moving it to that position |
| * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was |
| * 'stuck' in the target and needs to catch up to the touch location. |
| */ |
| private boolean mSpringingToTouch = false; |
| |
| /** |
| * Whether PIP was released in the dismiss target, and will be animated out and dismissed |
| * shortly. |
| */ |
| private boolean mDismissalPending = false; |
| |
| /** |
| * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is |
| * used to show menu activity when the expand animation is completed. |
| */ |
| private Runnable mPostPipTransitionCallback; |
| |
| private final PipTransitionController.PipTransitionCallback mPipTransitionCallback = |
| new PipTransitionController.PipTransitionCallback() { |
| @Override |
| public void onPipTransitionStarted(int direction, Rect pipBounds) {} |
| |
| @Override |
| public void onPipTransitionFinished(int direction) { |
| if (mPostPipTransitionCallback != null) { |
| mPostPipTransitionCallback.run(); |
| mPostPipTransitionCallback = null; |
| } |
| } |
| |
| @Override |
| public void onPipTransitionCanceled(int direction) {} |
| }; |
| |
| public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, |
| PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, |
| PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, |
| FloatingContentCoordinator floatingContentCoordinator) { |
| mContext = context; |
| mPipTaskOrganizer = pipTaskOrganizer; |
| mPipBoundsState = pipBoundsState; |
| mMenuController = menuController; |
| mSnapAlgorithm = snapAlgorithm; |
| mFloatingContentCoordinator = floatingContentCoordinator; |
| pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); |
| mResizePipUpdateListener = (target, values) -> { |
| if (mPipBoundsState.getMotionBoundsState().isInMotion()) { |
| mPipTaskOrganizer.scheduleUserResizePip(getBounds(), |
| mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null); |
| } |
| }; |
| } |
| |
| public void init() { |
| // Note: Needs to get the shell main thread sf vsync animation handler |
| mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( |
| mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); |
| mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( |
| mSfAnimationHandlerThreadLocal.get()); |
| } |
| |
| @NonNull |
| @Override |
| public Rect getFloatingBoundsOnScreen() { |
| return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() |
| ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); |
| } |
| |
| @NonNull |
| @Override |
| public Rect getAllowedFloatingBoundsRegion() { |
| return mFloatingAllowedArea; |
| } |
| |
| @Override |
| public void moveToBounds(@NonNull Rect bounds) { |
| animateToBounds(bounds, mConflictResolutionSpringConfig); |
| } |
| |
| /** |
| * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. |
| */ |
| void synchronizePinnedStackBounds() { |
| cancelPhysicsAnimation(); |
| mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); |
| |
| if (mPipTaskOrganizer.isInPip()) { |
| mFloatingContentCoordinator.onContentMoved(this); |
| } |
| } |
| |
| /** |
| * Tries to move the pinned stack to the given {@param bounds}. |
| */ |
| void movePip(Rect toBounds) { |
| movePip(toBounds, false /* isDragging */); |
| } |
| |
| /** |
| * Tries to move the pinned stack to the given {@param bounds}. |
| * |
| * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we |
| * won't notify the floating content coordinator of this move, since that will |
| * happen when the gesture ends. |
| */ |
| void movePip(Rect toBounds, boolean isDragging) { |
| if (!isDragging) { |
| mFloatingContentCoordinator.onContentMoved(this); |
| } |
| |
| if (!mSpringingToTouch) { |
| // If we are moving PIP directly to the touch event locations, cancel any animations and |
| // move PIP to the given bounds. |
| cancelPhysicsAnimation(); |
| |
| if (!isDragging) { |
| resizePipUnchecked(toBounds); |
| mPipBoundsState.setBounds(toBounds); |
| } else { |
| mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds); |
| mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds, |
| (Rect newBounds) -> { |
| mMenuController.updateMenuLayout(newBounds); |
| }); |
| } |
| } else { |
| // If PIP is 'catching up' after being stuck in the dismiss target, update the animation |
| // to spring towards the new touch location. |
| mTemporaryBoundsPhysicsAnimator |
| .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig) |
| .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig) |
| .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig) |
| .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig); |
| |
| startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */); |
| } |
| } |
| |
| /** Animates the PIP into the dismiss target, scaling it down. */ |
| void animateIntoDismissTarget( |
| MagnetizedObject.MagneticTarget target, |
| float velX, float velY, |
| boolean flung, Function0<Unit> after) { |
| final PointF targetCenter = target.getCenterOnScreen(); |
| |
| // PIP should fit in the circle |
| final float dismissCircleSize = mContext.getResources().getDimensionPixelSize( |
| R.dimen.dismiss_circle_size); |
| |
| final float width = getBounds().width(); |
| final float height = getBounds().height(); |
| final float ratio = width / height; |
| |
| // Width should be a little smaller than the circle size. |
| final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT; |
| final float desiredHeight = desiredWidth / ratio; |
| final float destinationX = targetCenter.x - (desiredWidth / 2f); |
| final float destinationY = targetCenter.y - (desiredHeight / 2f); |
| |
| // If we're already in the dismiss target area, then there won't be a move to set the |
| // temporary bounds, so just initialize it to the current bounds. |
| if (!mPipBoundsState.getMotionBoundsState().isInMotion()) { |
| mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); |
| } |
| mTemporaryBoundsPhysicsAnimator |
| .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig) |
| .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig) |
| .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig) |
| .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig) |
| .withEndActions(after); |
| |
| startBoundsAnimator(destinationX, destinationY); |
| } |
| |
| /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ |
| void setSpringingToTouch(boolean springingToTouch) { |
| mSpringingToTouch = springingToTouch; |
| } |
| |
| /** |
| * Resizes the pinned stack back to unknown windowing mode, which could be freeform or |
| * * fullscreen depending on the display area's windowing mode. |
| */ |
| void expandLeavePip() { |
| expandLeavePip(false /* skipAnimation */); |
| } |
| |
| /** |
| * Resizes the pinned stack back to unknown windowing mode, which could be freeform or |
| * fullscreen depending on the display area's windowing mode. |
| */ |
| void expandLeavePip(boolean skipAnimation) { |
| if (DEBUG) { |
| Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation |
| + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| cancelPhysicsAnimation(); |
| mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); |
| mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION); |
| } |
| |
| /** |
| * Dismisses the pinned stack. |
| */ |
| @Override |
| public void dismissPip() { |
| if (DEBUG) { |
| Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " ")); |
| } |
| cancelPhysicsAnimation(); |
| mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); |
| mPipTaskOrganizer.removePip(); |
| } |
| |
| /** Sets the movement bounds to use to constrain PIP position animations. */ |
| void onMovementBoundsChanged() { |
| rebuildFlingConfigs(); |
| |
| // The movement bounds represent the area within which we can move PIP's top-left position. |
| // The allowed area for all of PIP is those bounds plus PIP's width and height. |
| mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds()); |
| mFloatingAllowedArea.right += getBounds().width(); |
| mFloatingAllowedArea.bottom += getBounds().height(); |
| } |
| |
| /** |
| * @return the PiP bounds. |
| */ |
| private Rect getBounds() { |
| return mPipBoundsState.getBounds(); |
| } |
| |
| /** |
| * Flings the PiP to the closest snap target. |
| */ |
| void flingToSnapTarget( |
| float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) { |
| movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */); |
| } |
| |
| /** |
| * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion. |
| */ |
| void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) { |
| velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY; |
| movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */); |
| } |
| |
| private void movetoTarget( |
| float velocityX, |
| float velocityY, |
| @Nullable Runnable postBoundsUpdateCallback, |
| boolean isStash) { |
| // If we're flinging to a snap target now, we're not springing to catch up to the touch |
| // location now. |
| mSpringingToTouch = false; |
| |
| mTemporaryBoundsPhysicsAnimator |
| .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) |
| .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) |
| .flingThenSpring( |
| FloatProperties.RECT_X, velocityX, |
| isStash ? mStashConfigX : mFlingConfigX, |
| mSpringConfig, true /* flingMustReachMinOrMax */) |
| .flingThenSpring( |
| FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig); |
| |
| final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); |
| final float leftEdge = isStash |
| ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() |
| + insetBounds.left |
| : mPipBoundsState.getMovementBounds().left; |
| final float rightEdge = isStash |
| ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() |
| - insetBounds.right |
| : mPipBoundsState.getMovementBounds().right; |
| |
| final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; |
| |
| final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top; |
| final float estimatedFlingYEndValue = |
| PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY); |
| |
| startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, |
| postBoundsUpdateCallback); |
| } |
| |
| /** |
| * Animates PIP to the provided bounds, using physics animations and the given spring |
| * configuration |
| */ |
| void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { |
| if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { |
| // Animate from the current bounds if we're not already animating. |
| mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); |
| } |
| |
| mTemporaryBoundsPhysicsAnimator |
| .spring(FloatProperties.RECT_X, bounds.left, springConfig) |
| .spring(FloatProperties.RECT_Y, bounds.top, springConfig); |
| startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */); |
| } |
| |
| /** |
| * Animates the dismissal of the PiP off the edge of the screen. |
| */ |
| void animateDismiss() { |
| // Animate off the bottom of the screen, then dismiss PIP. |
| mTemporaryBoundsPhysicsAnimator |
| .spring(FloatProperties.RECT_Y, |
| mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, |
| 0, |
| mSpringConfig) |
| .withEndActions(this::dismissPip); |
| |
| startBoundsAnimator( |
| getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); |
| |
| mDismissalPending = false; |
| } |
| |
| /** |
| * Animates the PiP to the expanded state to show the menu. |
| */ |
| float animateToExpandedState(Rect expandedBounds, Rect movementBounds, |
| Rect expandedMovementBounds, Runnable callback) { |
| float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), |
| movementBounds); |
| mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); |
| mPostPipTransitionCallback = callback; |
| resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); |
| return savedSnapFraction; |
| } |
| |
| /** |
| * Animates the PiP from the expanded state to the normal state after the menu is hidden. |
| */ |
| void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, |
| Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { |
| if (savedSnapFraction < 0f) { |
| // If there are no saved snap fractions, then just use the current bounds |
| savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), |
| currentMovementBounds, mPipBoundsState.getStashedState()); |
| } |
| |
| mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction, |
| mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), |
| mPipBoundsState.getDisplayBounds(), |
| mPipBoundsState.getDisplayLayout().stableInsets()); |
| |
| if (immediate) { |
| movePip(normalBounds); |
| } else { |
| resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); |
| } |
| } |
| |
| /** |
| * Animates the PiP to the stashed state, choosing the closest edge. |
| */ |
| void animateToStashedClosestEdge() { |
| Rect tmpBounds = new Rect(); |
| final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); |
| final int stashType = |
| mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left |
| ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT; |
| final float leftEdge = stashType == STASH_TYPE_LEFT |
| ? mPipBoundsState.getStashOffset() |
| - mPipBoundsState.getBounds().width() + insetBounds.left |
| : mPipBoundsState.getDisplayBounds().right |
| - mPipBoundsState.getStashOffset() - insetBounds.right; |
| tmpBounds.set((int) leftEdge, |
| mPipBoundsState.getBounds().top, |
| (int) (leftEdge + mPipBoundsState.getBounds().width()), |
| mPipBoundsState.getBounds().bottom); |
| resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION); |
| mPipBoundsState.setStashed(stashType); |
| } |
| |
| /** |
| * Animates the PiP from stashed state into un-stashed, popping it out from the edge. |
| */ |
| void animateToUnStashedBounds(Rect unstashedBounds) { |
| resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION); |
| } |
| |
| /** |
| * Animates the PiP to offset it from the IME or shelf. |
| */ |
| void animateToOffset(Rect originalBounds, int offset) { |
| if (DEBUG) { |
| Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset |
| + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| cancelPhysicsAnimation(); |
| mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, |
| mUpdateBoundsCallback); |
| } |
| |
| /** |
| * Cancels all existing animations. |
| */ |
| private void cancelPhysicsAnimation() { |
| mTemporaryBoundsPhysicsAnimator.cancel(); |
| mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); |
| mSpringingToTouch = false; |
| } |
| |
| /** Set new fling configs whose min/max values respect the given movement bounds. */ |
| private void rebuildFlingConfigs() { |
| mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, |
| mPipBoundsState.getMovementBounds().left, |
| mPipBoundsState.getMovementBounds().right); |
| mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, |
| mPipBoundsState.getMovementBounds().top, |
| mPipBoundsState.getMovementBounds().bottom); |
| final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); |
| mStashConfigX = new PhysicsAnimator.FlingConfig( |
| DEFAULT_FRICTION, |
| mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() |
| + insetBounds.left, |
| mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() |
| - insetBounds.right); |
| } |
| |
| private void startBoundsAnimator(float toX, float toY) { |
| startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */); |
| } |
| |
| /** |
| * 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. |
| * |
| * 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. |
| * |
| * If one wishes to supply a callback after all the 'real' bounds update has happened, |
| * pass @param postBoundsUpdateCallback. |
| */ |
| private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) { |
| if (!mSpringingToTouch) { |
| cancelPhysicsAnimation(); |
| } |
| |
| setAnimatingToBounds(new Rect( |
| (int) toX, |
| (int) toY, |
| (int) toX + getBounds().width(), |
| (int) toY + getBounds().height())); |
| |
| if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { |
| if (postBoundsUpdateCallback != null) { |
| mTemporaryBoundsPhysicsAnimator |
| .addUpdateListener(mResizePipUpdateListener) |
| .withEndActions(this::onBoundsPhysicsAnimationEnd, |
| postBoundsUpdateCallback); |
| } else { |
| mTemporaryBoundsPhysicsAnimator |
| .addUpdateListener(mResizePipUpdateListener) |
| .withEndActions(this::onBoundsPhysicsAnimationEnd); |
| } |
| } |
| |
| mTemporaryBoundsPhysicsAnimator.start(); |
| } |
| |
| /** |
| * Notify that PIP was released in the dismiss target and will be animated out and dismissed |
| * shortly. |
| */ |
| void notifyDismissalPending() { |
| mDismissalPending = true; |
| } |
| |
| private void onBoundsPhysicsAnimationEnd() { |
| // The physics animation ended, though we may not necessarily be done animating, such as |
| // when we're still dragging after moving out of the magnetic target. |
| if (!mDismissalPending |
| && !mSpringingToTouch |
| && !mMagnetizedPip.getObjectStuckToTarget()) { |
| // All motion operations have actually finished. |
| mPipBoundsState.setBounds( |
| mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); |
| mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); |
| if (!mDismissalPending) { |
| // do not schedule resize if PiP is dismissing, which may cause app re-open to |
| // mBounds instead of it's normal bounds. |
| mPipTaskOrganizer.scheduleFinishResizePip(getBounds()); |
| } |
| } |
| mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); |
| mSpringingToTouch = false; |
| mDismissalPending = false; |
| } |
| |
| /** |
| * Notifies the floating coordinator that we're moving, and sets the animating to bounds so |
| * we return these bounds from |
| * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. |
| */ |
| private void setAnimatingToBounds(Rect bounds) { |
| mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds); |
| mFloatingContentCoordinator.onContentMoved(this); |
| } |
| |
| /** |
| * Directly resizes the PiP to the given {@param bounds}. |
| */ |
| private void resizePipUnchecked(Rect toBounds) { |
| if (DEBUG) { |
| Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds |
| + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| if (!toBounds.equals(getBounds())) { |
| mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); |
| } |
| } |
| |
| /** |
| * Directly resizes the PiP to the given {@param bounds}. |
| */ |
| private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { |
| if (DEBUG) { |
| Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds |
| + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| |
| // Intentionally resize here even if the current bounds match the destination bounds. |
| // This is so all the proper callbacks are performed. |
| mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, |
| TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); |
| setAnimatingToBounds(toBounds); |
| } |
| |
| /** |
| * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the |
| * magnetic dismiss target so it can calculate PIP's size and position. |
| */ |
| MagnetizedObject<Rect> getMagnetizedPip() { |
| if (mMagnetizedPip == null) { |
| mMagnetizedPip = new MagnetizedObject<Rect>( |
| mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), |
| FloatProperties.RECT_X, FloatProperties.RECT_Y) { |
| @Override |
| public float getWidth(@NonNull Rect animatedPipBounds) { |
| return animatedPipBounds.width(); |
| } |
| |
| @Override |
| public float getHeight(@NonNull Rect animatedPipBounds) { |
| return animatedPipBounds.height(); |
| } |
| |
| @Override |
| public void getLocationOnScreen( |
| @NonNull Rect animatedPipBounds, @NonNull int[] loc) { |
| loc[0] = animatedPipBounds.left; |
| loc[1] = animatedPipBounds.top; |
| } |
| }; |
| } |
| |
| return mMagnetizedPip; |
| } |
| } |