1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip.phone; 18 19 import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY; 20 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; 21 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; 22 23 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; 24 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; 25 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; 26 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; 27 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_DISMISS; 28 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; 29 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.content.Context; 33 import android.graphics.PointF; 34 import android.graphics.Rect; 35 import android.os.Debug; 36 37 import com.android.internal.protolog.common.ProtoLog; 38 import com.android.wm.shell.R; 39 import com.android.wm.shell.animation.FloatProperties; 40 import com.android.wm.shell.animation.PhysicsAnimator; 41 import com.android.wm.shell.common.FloatingContentCoordinator; 42 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 43 import com.android.wm.shell.common.pip.PipAppOpsListener; 44 import com.android.wm.shell.common.pip.PipBoundsState; 45 import com.android.wm.shell.common.pip.PipSnapAlgorithm; 46 import com.android.wm.shell.pip.PipTaskOrganizer; 47 import com.android.wm.shell.pip.PipTransitionController; 48 import com.android.wm.shell.protolog.ShellProtoLogGroup; 49 50 import kotlin.Unit; 51 import kotlin.jvm.functions.Function0; 52 53 import java.util.function.Consumer; 54 55 /** 56 * A helper to animate and manipulate the PiP. 57 */ 58 public class PipMotionHelper implements PipAppOpsListener.Callback, 59 FloatingContentCoordinator.FloatingContent { 60 private static final String TAG = "PipMotionHelper"; 61 private static final boolean DEBUG = false; 62 63 private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; 64 private static final int EXPAND_STACK_TO_MENU_DURATION = 250; 65 private static final int UNSTASH_DURATION = 250; 66 private static final int LEAVE_PIP_DURATION = 300; 67 private static final int SHIFT_DURATION = 300; 68 69 /** Friction to use for PIP when it moves via physics fling animations. */ 70 private static final float DEFAULT_FRICTION = 1.9f; 71 /** How much of the dismiss circle size to use when scaling down PIP. **/ 72 private static final float DISMISS_CIRCLE_PERCENT = 0.85f; 73 74 private final Context mContext; 75 private final PipTaskOrganizer mPipTaskOrganizer; 76 private @NonNull PipBoundsState mPipBoundsState; 77 78 private PhonePipMenuController mMenuController; 79 private PipSnapAlgorithm mSnapAlgorithm; 80 81 /** The region that all of PIP must stay within. */ 82 private final Rect mFloatingAllowedArea = new Rect(); 83 84 /** Coordinator instance for resolving conflicts with other floating content. */ 85 private FloatingContentCoordinator mFloatingContentCoordinator; 86 87 /** 88 * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} 89 * using physics animations. 90 */ 91 private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator; 92 93 private MagnetizedObject<Rect> mMagnetizedPip; 94 95 /** 96 * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}. 97 */ 98 private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; 99 100 /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ 101 private PhysicsAnimator.FlingConfig mFlingConfigX; 102 private PhysicsAnimator.FlingConfig mFlingConfigY; 103 /** FlingConfig instances proviced to PhysicsAnimator for stashing. */ 104 private PhysicsAnimator.FlingConfig mStashConfigX; 105 106 /** SpringConfig to use for fling-then-spring animations. */ 107 private final PhysicsAnimator.SpringConfig mSpringConfig = 108 new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY); 109 110 /** SpringConfig used for animating into the dismiss region, matches the one in 111 * {@link MagnetizedObject}. */ 112 private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig = 113 new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY); 114 115 /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss 116 * drag region. */ 117 private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig = 118 new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY); 119 120 /** SpringConfig to use for springing PIP away from conflicting floating content. */ 121 private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = 122 new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); 123 124 private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { 125 if (mPipBoundsState.getBounds().equals(newBounds)) { 126 return; 127 } 128 129 mMenuController.updateMenuLayout(newBounds); 130 mPipBoundsState.setBounds(newBounds); 131 }; 132 133 /** 134 * Whether we're springing to the touch event location (vs. moving it to that position 135 * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was 136 * 'stuck' in the target and needs to catch up to the touch location. 137 */ 138 private boolean mSpringingToTouch = false; 139 140 /** 141 * Whether PIP was released in the dismiss target, and will be animated out and dismissed 142 * shortly. 143 */ 144 private boolean mDismissalPending = false; 145 146 /** 147 * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is 148 * used to show menu activity when the expand animation is completed. 149 */ 150 private Runnable mPostPipTransitionCallback; 151 152 private final PipTransitionController.PipTransitionCallback mPipTransitionCallback = 153 new PipTransitionController.PipTransitionCallback() { 154 @Override 155 public void onPipTransitionStarted(int direction, Rect pipBounds) {} 156 157 @Override 158 public void onPipTransitionFinished(int direction) { 159 if (mPostPipTransitionCallback != null) { 160 mPostPipTransitionCallback.run(); 161 mPostPipTransitionCallback = null; 162 } 163 } 164 165 @Override 166 public void onPipTransitionCanceled(int direction) {} 167 }; 168 PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator)169 public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, 170 PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, 171 PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, 172 FloatingContentCoordinator floatingContentCoordinator) { 173 mContext = context; 174 mPipTaskOrganizer = pipTaskOrganizer; 175 mPipBoundsState = pipBoundsState; 176 mMenuController = menuController; 177 mSnapAlgorithm = snapAlgorithm; 178 mFloatingContentCoordinator = floatingContentCoordinator; 179 pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); 180 mResizePipUpdateListener = (target, values) -> { 181 if (mPipBoundsState.getMotionBoundsState().isInMotion()) { 182 mPipTaskOrganizer.scheduleUserResizePip(getBounds(), 183 mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null); 184 } 185 }; 186 } 187 init()188 public void init() { 189 mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( 190 mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); 191 } 192 193 @NonNull 194 @Override getFloatingBoundsOnScreen()195 public Rect getFloatingBoundsOnScreen() { 196 return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() 197 ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); 198 } 199 200 @NonNull 201 @Override getAllowedFloatingBoundsRegion()202 public Rect getAllowedFloatingBoundsRegion() { 203 return mFloatingAllowedArea; 204 } 205 206 @Override moveToBounds(@onNull Rect bounds)207 public void moveToBounds(@NonNull Rect bounds) { 208 animateToBounds(bounds, mConflictResolutionSpringConfig); 209 } 210 211 /** 212 * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. 213 */ synchronizePinnedStackBounds()214 void synchronizePinnedStackBounds() { 215 cancelPhysicsAnimation(); 216 mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); 217 218 if (mPipTaskOrganizer.isInPip()) { 219 mFloatingContentCoordinator.onContentMoved(this); 220 } 221 } 222 223 /** 224 * Tries to move the pinned stack to the given {@param bounds}. 225 */ movePip(Rect toBounds)226 void movePip(Rect toBounds) { 227 movePip(toBounds, false /* isDragging */); 228 } 229 230 /** 231 * Tries to move the pinned stack to the given {@param bounds}. 232 * 233 * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we 234 * won't notify the floating content coordinator of this move, since that will 235 * happen when the gesture ends. 236 */ movePip(Rect toBounds, boolean isDragging)237 void movePip(Rect toBounds, boolean isDragging) { 238 if (!isDragging) { 239 mFloatingContentCoordinator.onContentMoved(this); 240 } 241 242 if (!mSpringingToTouch) { 243 // If we are moving PIP directly to the touch event locations, cancel any animations and 244 // move PIP to the given bounds. 245 cancelPhysicsAnimation(); 246 247 if (!isDragging) { 248 resizePipUnchecked(toBounds); 249 mPipBoundsState.setBounds(toBounds); 250 } else { 251 mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds); 252 mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds, 253 (Rect newBounds) -> { 254 mMenuController.updateMenuLayout(newBounds); 255 }); 256 } 257 } else { 258 // If PIP is 'catching up' after being stuck in the dismiss target, update the animation 259 // to spring towards the new touch location. 260 mTemporaryBoundsPhysicsAnimator 261 .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig) 262 .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig) 263 .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig) 264 .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig); 265 266 startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */); 267 } 268 } 269 270 /** Animates the PIP into the dismiss target, scaling it down. */ animateIntoDismissTarget( MagnetizedObject.MagneticTarget target, float velX, float velY, boolean flung, Function0<Unit> after)271 void animateIntoDismissTarget( 272 MagnetizedObject.MagneticTarget target, 273 float velX, float velY, 274 boolean flung, Function0<Unit> after) { 275 final PointF targetCenter = target.getCenterOnScreen(); 276 277 // PIP should fit in the circle 278 final float dismissCircleSize = mContext.getResources().getDimensionPixelSize( 279 R.dimen.dismiss_circle_size); 280 281 final float width = getBounds().width(); 282 final float height = getBounds().height(); 283 final float ratio = width / height; 284 285 // Width should be a little smaller than the circle size. 286 final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT; 287 final float desiredHeight = desiredWidth / ratio; 288 final float destinationX = targetCenter.x - (desiredWidth / 2f); 289 final float destinationY = targetCenter.y - (desiredHeight / 2f); 290 291 // If we're already in the dismiss target area, then there won't be a move to set the 292 // temporary bounds, so just initialize it to the current bounds. 293 if (!mPipBoundsState.getMotionBoundsState().isInMotion()) { 294 mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); 295 } 296 mTemporaryBoundsPhysicsAnimator 297 .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig) 298 .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig) 299 .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig) 300 .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig) 301 .withEndActions(after); 302 303 startBoundsAnimator(destinationX, destinationY); 304 } 305 306 /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ setSpringingToTouch(boolean springingToTouch)307 void setSpringingToTouch(boolean springingToTouch) { 308 mSpringingToTouch = springingToTouch; 309 } 310 311 /** 312 * Resizes the pinned stack back to unknown windowing mode, which could be freeform or 313 * * fullscreen depending on the display area's windowing mode. 314 */ expandLeavePip(boolean skipAnimation)315 void expandLeavePip(boolean skipAnimation) { 316 expandLeavePip(skipAnimation, false /* enterSplit */); 317 } 318 319 /** 320 * Resizes the pinned task to split-screen mode. 321 */ expandIntoSplit()322 void expandIntoSplit() { 323 expandLeavePip(false, true /* enterSplit */); 324 } 325 326 /** 327 * Resizes the pinned stack back to unknown windowing mode, which could be freeform or 328 * fullscreen depending on the display area's windowing mode. 329 */ expandLeavePip(boolean skipAnimation, boolean enterSplit)330 private void expandLeavePip(boolean skipAnimation, boolean enterSplit) { 331 if (DEBUG) { 332 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 333 "%s: exitPip: skipAnimation=%s" 334 + " callers=\n%s", TAG, skipAnimation, Debug.getCallers(5, " ")); 335 } 336 cancelPhysicsAnimation(); 337 mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); 338 mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit); 339 } 340 341 /** 342 * Dismisses the pinned stack. 343 */ 344 @Override dismissPip()345 public void dismissPip() { 346 if (DEBUG) { 347 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 348 "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " ")); 349 } 350 cancelPhysicsAnimation(); 351 mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); 352 mPipTaskOrganizer.removePip(); 353 } 354 355 /** Sets the movement bounds to use to constrain PIP position animations. */ onMovementBoundsChanged()356 void onMovementBoundsChanged() { 357 rebuildFlingConfigs(); 358 359 // The movement bounds represent the area within which we can move PIP's top-left position. 360 // The allowed area for all of PIP is those bounds plus PIP's width and height. 361 mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds()); 362 mFloatingAllowedArea.right += getBounds().width(); 363 mFloatingAllowedArea.bottom += getBounds().height(); 364 } 365 366 /** 367 * @return the PiP bounds. 368 */ getBounds()369 private Rect getBounds() { 370 return mPipBoundsState.getBounds(); 371 } 372 373 /** 374 * Flings the PiP to the closest snap target. 375 */ flingToSnapTarget( float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback)376 void flingToSnapTarget( 377 float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) { 378 movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */); 379 } 380 381 /** 382 * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion. 383 */ stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback)384 void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) { 385 velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY; 386 movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */); 387 } 388 movetoTarget( float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback, boolean isStash)389 private void movetoTarget( 390 float velocityX, 391 float velocityY, 392 @Nullable Runnable postBoundsUpdateCallback, 393 boolean isStash) { 394 // If we're flinging to a snap target now, we're not springing to catch up to the touch 395 // location now. 396 mSpringingToTouch = false; 397 398 mTemporaryBoundsPhysicsAnimator 399 .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) 400 .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) 401 .flingThenSpring( 402 FloatProperties.RECT_X, velocityX, 403 isStash ? mStashConfigX : mFlingConfigX, 404 mSpringConfig, true /* flingMustReachMinOrMax */) 405 .flingThenSpring( 406 FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig); 407 408 final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); 409 final float leftEdge = isStash 410 ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() 411 + insetBounds.left 412 : mPipBoundsState.getMovementBounds().left; 413 final float rightEdge = isStash 414 ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() 415 - insetBounds.right 416 : mPipBoundsState.getMovementBounds().right; 417 418 final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; 419 420 final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top; 421 final float estimatedFlingYEndValue = 422 PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY); 423 424 startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, 425 postBoundsUpdateCallback); 426 } 427 428 /** 429 * Animates PIP to the provided bounds, using physics animations and the given spring 430 * configuration 431 */ 432 void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { 433 if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { 434 // Animate from the current bounds if we're not already animating. 435 mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); 436 } 437 438 mTemporaryBoundsPhysicsAnimator 439 .spring(FloatProperties.RECT_X, bounds.left, springConfig) 440 .spring(FloatProperties.RECT_Y, bounds.top, springConfig); 441 startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */); 442 } 443 444 /** 445 * Animates the dismissal of the PiP off the edge of the screen. 446 */ 447 void animateDismiss() { 448 // Animate off the bottom of the screen, then dismiss PIP. 449 mTemporaryBoundsPhysicsAnimator 450 .spring(FloatProperties.RECT_Y, 451 mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, 452 0, 453 mSpringConfig) 454 .withEndActions(this::dismissPip); 455 456 startBoundsAnimator( 457 getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); 458 459 mDismissalPending = false; 460 } 461 462 /** 463 * Animates the PiP to the expanded state to show the menu. 464 */ 465 float animateToExpandedState(Rect expandedBounds, Rect movementBounds, 466 Rect expandedMovementBounds, Runnable callback) { 467 float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), 468 movementBounds); 469 mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); 470 mPostPipTransitionCallback = callback; 471 resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); 472 return savedSnapFraction; 473 } 474 475 /** 476 * Animates the PiP from the expanded state to the normal state after the menu is hidden. 477 */ 478 void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, 479 Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { 480 if (savedSnapFraction < 0f) { 481 // If there are no saved snap fractions, then just use the current bounds 482 savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), 483 currentMovementBounds, mPipBoundsState.getStashedState()); 484 } 485 486 mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction, 487 mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), 488 mPipBoundsState.getDisplayBounds(), 489 mPipBoundsState.getDisplayLayout().stableInsets()); 490 491 if (immediate) { 492 movePip(normalBounds); 493 } else { 494 resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); 495 } 496 } 497 498 /** 499 * Animates the PiP to the stashed state, choosing the closest edge. 500 */ 501 void animateToStashedClosestEdge() { 502 Rect tmpBounds = new Rect(); 503 final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); 504 final int stashType = 505 mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left 506 ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT; 507 final float leftEdge = stashType == STASH_TYPE_LEFT 508 ? mPipBoundsState.getStashOffset() 509 - mPipBoundsState.getBounds().width() + insetBounds.left 510 : mPipBoundsState.getDisplayBounds().right 511 - mPipBoundsState.getStashOffset() - insetBounds.right; 512 tmpBounds.set((int) leftEdge, 513 mPipBoundsState.getBounds().top, 514 (int) (leftEdge + mPipBoundsState.getBounds().width()), 515 mPipBoundsState.getBounds().bottom); 516 resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION); 517 mPipBoundsState.setStashed(stashType); 518 } 519 520 /** 521 * Animates the PiP from stashed state into un-stashed, popping it out from the edge. 522 */ 523 void animateToUnStashedBounds(Rect unstashedBounds) { 524 resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION); 525 } 526 527 /** 528 * Animates the PiP to offset it from the IME or shelf. 529 */ 530 void animateToOffset(Rect originalBounds, int offset) { 531 if (DEBUG) { 532 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 533 "%s: animateToOffset: originalBounds=%s offset=%s" 534 + " callers=\n%s", TAG, originalBounds, offset, 535 Debug.getCallers(5, " ")); 536 } 537 cancelPhysicsAnimation(); 538 mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, 539 mUpdateBoundsCallback); 540 } 541 542 /** 543 * Cancels all existing animations. 544 */ 545 private void cancelPhysicsAnimation() { 546 mTemporaryBoundsPhysicsAnimator.cancel(); 547 mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); 548 mSpringingToTouch = false; 549 } 550 551 /** Set new fling configs whose min/max values respect the given movement bounds. */ 552 private void rebuildFlingConfigs() { 553 mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, 554 mPipBoundsState.getMovementBounds().left, 555 mPipBoundsState.getMovementBounds().right); 556 mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, 557 mPipBoundsState.getMovementBounds().top, 558 mPipBoundsState.getMovementBounds().bottom); 559 final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); 560 mStashConfigX = new PhysicsAnimator.FlingConfig( 561 DEFAULT_FRICTION, 562 mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() 563 + insetBounds.left, 564 mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() 565 - insetBounds.right); 566 } 567 568 private void startBoundsAnimator(float toX, float toY) { 569 startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */); 570 } 571 572 /** 573 * Starts the physics animator which will update the animated PIP bounds using physics 574 * animations, as well as the TimeAnimator which will apply those bounds to PIP. 575 * 576 * This will also add end actions to the bounds animator that cancel the TimeAnimator and update 577 * the 'real' bounds to equal the final animated bounds. 578 * 579 * If one wishes to supply a callback after all the 'real' bounds update has happened, 580 * pass @param postBoundsUpdateCallback. 581 */ 582 private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) { 583 if (!mSpringingToTouch) { 584 cancelPhysicsAnimation(); 585 } 586 587 setAnimatingToBounds(new Rect( 588 (int) toX, 589 (int) toY, 590 (int) toX + getBounds().width(), 591 (int) toY + getBounds().height())); 592 593 if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { 594 if (postBoundsUpdateCallback != null) { 595 mTemporaryBoundsPhysicsAnimator 596 .addUpdateListener(mResizePipUpdateListener) 597 .withEndActions(this::onBoundsPhysicsAnimationEnd, 598 postBoundsUpdateCallback); 599 } else { 600 mTemporaryBoundsPhysicsAnimator 601 .addUpdateListener(mResizePipUpdateListener) 602 .withEndActions(this::onBoundsPhysicsAnimationEnd); 603 } 604 } 605 606 mTemporaryBoundsPhysicsAnimator.start(); 607 } 608 609 /** 610 * Notify that PIP was released in the dismiss target and will be animated out and dismissed 611 * shortly. 612 */ 613 void notifyDismissalPending() { 614 mDismissalPending = true; 615 } 616 617 private void onBoundsPhysicsAnimationEnd() { 618 // The physics animation ended, though we may not necessarily be done animating, such as 619 // when we're still dragging after moving out of the magnetic target. 620 if (!mDismissalPending 621 && !mSpringingToTouch 622 && !mMagnetizedPip.getObjectStuckToTarget()) { 623 // All motion operations have actually finished. 624 mPipBoundsState.setBounds( 625 mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); 626 mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); 627 if (!mDismissalPending) { 628 // do not schedule resize if PiP is dismissing, which may cause app re-open to 629 // mBounds instead of it's normal bounds. 630 mPipTaskOrganizer.scheduleFinishResizePip(getBounds()); 631 } 632 } 633 mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); 634 mSpringingToTouch = false; 635 mDismissalPending = false; 636 } 637 638 /** 639 * Notifies the floating coordinator that we're moving, and sets the animating to bounds so 640 * we return these bounds from 641 * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. 642 */ 643 private void setAnimatingToBounds(Rect bounds) { 644 mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds); 645 mFloatingContentCoordinator.onContentMoved(this); 646 } 647 648 /** 649 * Directly resizes the PiP to the given {@param bounds}. 650 */ 651 private void resizePipUnchecked(Rect toBounds) { 652 if (DEBUG) { 653 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 654 "%s: resizePipUnchecked: toBounds=%s" 655 + " callers=\n%s", TAG, toBounds, Debug.getCallers(5, " ")); 656 } 657 if (!toBounds.equals(getBounds())) { 658 mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); 659 } 660 } 661 662 /** 663 * Directly resizes the PiP to the given {@param bounds}. 664 */ 665 private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { 666 if (DEBUG) { 667 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 668 "%s: resizeAndAnimatePipUnchecked: toBounds=%s" 669 + " duration=%s callers=\n%s", TAG, toBounds, duration, 670 Debug.getCallers(5, " ")); 671 } 672 673 // Intentionally resize here even if the current bounds match the destination bounds. 674 // This is so all the proper callbacks are performed. 675 mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, 676 TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); 677 setAnimatingToBounds(toBounds); 678 } 679 680 /** 681 * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the 682 * magnetic dismiss target so it can calculate PIP's size and position. 683 */ 684 MagnetizedObject<Rect> getMagnetizedPip() { 685 if (mMagnetizedPip == null) { 686 mMagnetizedPip = new MagnetizedObject<Rect>( 687 mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), 688 FloatProperties.RECT_X, FloatProperties.RECT_Y) { 689 @Override 690 public float getWidth(@NonNull Rect animatedPipBounds) { 691 return animatedPipBounds.width(); 692 } 693 694 @Override 695 public float getHeight(@NonNull Rect animatedPipBounds) { 696 return animatedPipBounds.height(); 697 } 698 699 @Override 700 public void getLocationOnScreen( 701 @NonNull Rect animatedPipBounds, @NonNull int[] loc) { 702 loc[0] = animatedPipBounds.left; 703 loc[1] = animatedPipBounds.top; 704 } 705 }; 706 mMagnetizedPip.setFlingToTargetEnabled(false); 707 } 708 709 return mMagnetizedPip; 710 } 711 } 712