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