1 /* 2 * Copyright (C) 2019 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.bubbles.animation; 18 19 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; 20 21 import android.content.ContentResolver; 22 import android.content.res.Resources; 23 import android.graphics.PointF; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.provider.Settings; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.ViewPropertyAnimator; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.dynamicanimation.animation.DynamicAnimation; 34 import androidx.dynamicanimation.animation.FlingAnimation; 35 import androidx.dynamicanimation.animation.FloatPropertyCompat; 36 import androidx.dynamicanimation.animation.SpringAnimation; 37 import androidx.dynamicanimation.animation.SpringForce; 38 39 import com.android.wm.shell.R; 40 import com.android.wm.shell.animation.PhysicsAnimator; 41 import com.android.wm.shell.bubbles.BadgedImageView; 42 import com.android.wm.shell.bubbles.BubblePositioner; 43 import com.android.wm.shell.bubbles.BubbleStackView; 44 import com.android.wm.shell.common.FloatingContentCoordinator; 45 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 46 47 import com.google.android.collect.Sets; 48 49 import java.io.FileDescriptor; 50 import java.io.PrintWriter; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Set; 54 import java.util.function.IntSupplier; 55 56 /** 57 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop 58 * each other with a slight offset to the left or right (depending on which side of the screen they 59 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of 60 * the screen. 61 */ 62 public class StackAnimationController extends 63 PhysicsAnimationLayout.PhysicsAnimationController { 64 65 private static final String TAG = "Bubbs.StackCtrl"; 66 67 /** Value to use for animating bubbles in and springing stack after fling. */ 68 private static final float STACK_SPRING_STIFFNESS = 700f; 69 70 /** Values to use for animating updated bubble to top of stack. */ 71 private static final float NEW_BUBBLE_START_SCALE = 0.5f; 72 private static final float NEW_BUBBLE_START_Y = 100f; 73 private static final long BUBBLE_SWAP_DURATION = 300L; 74 75 /** 76 * Values to use for the default {@link SpringForce} provided to the physics animation layout. 77 */ 78 public static final int SPRING_TO_TOUCH_STIFFNESS = 12000; 79 public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW; 80 private static final int CHAIN_STIFFNESS = 800; 81 public static final float DEFAULT_BOUNCINESS = 0.9f; 82 83 private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = 84 new PhysicsAnimator.SpringConfig( 85 STACK_SPRING_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); 86 87 /** 88 * Friction applied to fling animations. Since the stack must land on one of the sides of the 89 * screen, we want less friction horizontally so that the stack has a better chance of making it 90 * to the side without needing a spring. 91 */ 92 private static final float FLING_FRICTION = 1.9f; 93 94 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; 95 96 /** Sentinel value for unset position value. */ 97 private static final float UNSET = -Float.MIN_VALUE; 98 99 /** 100 * Minimum fling velocity required to trigger moving the stack from one side of the screen to 101 * the other. 102 */ 103 private static final float ESCAPE_VELOCITY = 750f; 104 105 /** Velocity required to dismiss the stack without dragging it into the dismiss target. */ 106 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f; 107 108 /** 109 * The canonical position of the stack. This is typically the position of the first bubble, but 110 * we need to keep track of it separately from the first bubble's translation in case there are 111 * no bubbles, or the first bubble was just added and being animated to its new position. 112 */ 113 private PointF mStackPosition = new PointF(-1, -1); 114 115 /** 116 * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic 117 * dismiss target. 118 */ 119 private MagnetizedObject<StackAnimationController> mMagnetizedStack; 120 121 /** 122 * The area that Bubbles will occupy after all animations end. This is used to move other 123 * floating content out of the way proactively. 124 */ 125 private Rect mAnimatingToBounds = new Rect(); 126 127 /** Whether or not the stack's start position has been set. */ 128 private boolean mStackMovedToStartPosition = false; 129 130 /** The height of the most recently visible IME. */ 131 private float mImeHeight = 0f; 132 133 /** 134 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the 135 * IME is not visible or the user moved the stack since the IME became visible. 136 */ 137 private float mPreImeY = UNSET; 138 139 /** 140 * Animations on the stack position itself, which would have been started in 141 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to 142 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect) 143 * to a legal position on the side of the screen. 144 */ 145 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations = 146 new HashMap<>(); 147 148 /** 149 * Whether the current motion of the stack is due to a fling animation (vs. being dragged 150 * manually). 151 */ 152 private boolean mIsMovingFromFlinging = false; 153 154 /** 155 * Whether the first bubble is springing towards the touch point, rather than using the default 156 * behavior of moving directly to the touch point with the rest of the stack following it. 157 * 158 * This happens when the user's finger exits the dismiss area while the stack is magnetized to 159 * the center. Since the touch point differs from the stack location, we need to animate the 160 * stack back to the touch point to avoid a jarring instant location change from the center of 161 * the target to the touch point just outside the target bounds. 162 * 163 * This is reset once the spring animations end, since that means the first bubble has 164 * successfully 'caught up' to the touch. 165 */ 166 private boolean mFirstBubbleSpringingToTouch = false; 167 168 /** 169 * Whether to spring the stack to the next touch event coordinates. This is used to animate the 170 * stack (including the first bubble) out of the magnetic dismiss target to the touch location. 171 * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly 172 * and only animating the following bubbles. 173 */ 174 private boolean mSpringToTouchOnNextMotionEvent = false; 175 176 /** Horizontal offset of bubbles in the stack. */ 177 private float mStackOffset; 178 /** Offset between stack y and animation y for bubble swap. */ 179 private float mSwapAnimationOffset; 180 /** Max number of bubbles to show in the expanded bubble row. */ 181 private int mMaxBubbles; 182 /** Default bubble elevation. */ 183 private int mElevation; 184 /** Diameter of the bubble. */ 185 private int mBubbleSize; 186 /** 187 * The amount of space to add between the bubbles and certain UI elements, such as the top of 188 * the screen or the IME. This does not apply to the left/right sides of the screen since the 189 * stack goes offscreen intentionally. 190 */ 191 private int mBubblePaddingTop; 192 /** How far offscreen the stack rests. */ 193 private int mBubbleOffscreen; 194 /** Contains display size, orientation, and inset information. */ 195 private BubblePositioner mPositioner; 196 197 /** FloatingContentCoordinator instance for resolving floating content conflicts. */ 198 private FloatingContentCoordinator mFloatingContentCoordinator; 199 200 /** 201 * FloatingContent instance that returns the stack's location on the screen, and moves it when 202 * requested. 203 */ 204 private final FloatingContentCoordinator.FloatingContent mStackFloatingContent = 205 new FloatingContentCoordinator.FloatingContent() { 206 207 private final Rect mFloatingBoundsOnScreen = new Rect(); 208 209 @Override 210 public void moveToBounds(@NonNull Rect bounds) { 211 springStack(bounds.left, bounds.top, STACK_SPRING_STIFFNESS); 212 } 213 214 @NonNull 215 @Override 216 public Rect getAllowedFloatingBoundsRegion() { 217 final Rect floatingBounds = getFloatingBoundsOnScreen(); 218 final Rect allowableStackArea = new Rect(); 219 getAllowableStackPositionRegion().roundOut(allowableStackArea); 220 allowableStackArea.right += floatingBounds.width(); 221 allowableStackArea.bottom += floatingBounds.height(); 222 return allowableStackArea; 223 } 224 225 @NonNull 226 @Override 227 public Rect getFloatingBoundsOnScreen() { 228 if (!mAnimatingToBounds.isEmpty()) { 229 return mAnimatingToBounds; 230 } 231 232 if (mLayout.getChildCount() > 0) { 233 // Calculate the bounds using stack position + bubble size so that we don't need to 234 // wait for the bubble views to lay out. 235 mFloatingBoundsOnScreen.set( 236 (int) mStackPosition.x, 237 (int) mStackPosition.y, 238 (int) mStackPosition.x + mBubbleSize, 239 (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop); 240 } else { 241 mFloatingBoundsOnScreen.setEmpty(); 242 } 243 244 return mFloatingBoundsOnScreen; 245 } 246 }; 247 248 /** Returns the number of 'real' bubbles (excluding the overflow bubble). */ 249 private IntSupplier mBubbleCountSupplier; 250 251 /** 252 * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the 253 * end of this animation means we have no bubbles left, and notify the BubbleController. 254 */ 255 private Runnable mOnBubbleAnimatedOutAction; 256 257 /** 258 * Callback to run whenever the stack is finished being flung somewhere. 259 */ 260 private Runnable mOnStackAnimationFinished; 261 StackAnimationController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, Runnable onBubbleAnimatedOutAction, Runnable onStackAnimationFinished, BubblePositioner positioner)262 public StackAnimationController( 263 FloatingContentCoordinator floatingContentCoordinator, 264 IntSupplier bubbleCountSupplier, 265 Runnable onBubbleAnimatedOutAction, 266 Runnable onStackAnimationFinished, 267 BubblePositioner positioner) { 268 mFloatingContentCoordinator = floatingContentCoordinator; 269 mBubbleCountSupplier = bubbleCountSupplier; 270 mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; 271 mOnStackAnimationFinished = onStackAnimationFinished; 272 mPositioner = positioner; 273 } 274 275 /** 276 * Instantly move the first bubble to the given point, and animate the rest of the stack behind 277 * it with the 'following' effect. 278 */ moveFirstBubbleWithStackFollowing(float x, float y)279 public void moveFirstBubbleWithStackFollowing(float x, float y) { 280 // If we're moving the bubble around, we're not animating to any bounds. 281 mAnimatingToBounds.setEmpty(); 282 283 // If we manually move the bubbles with the IME open, clear the return point since we don't 284 // want the stack to snap away from the new position. 285 mPreImeY = UNSET; 286 287 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x); 288 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y); 289 290 // This method is called when the stack is being dragged manually, so we're clearly no 291 // longer flinging. 292 mIsMovingFromFlinging = false; 293 } 294 295 /** 296 * The position of the stack - typically the position of the first bubble; if no bubbles have 297 * been added yet, it will be where the first bubble will go when added. 298 */ getStackPosition()299 public PointF getStackPosition() { 300 return mStackPosition; 301 } 302 303 /** Whether the stack is on the left side of the screen. */ isStackOnLeftSide()304 public boolean isStackOnLeftSide() { 305 if (mLayout == null || !isStackPositionSet()) { 306 return true; // Default to left, which is where it starts by default. 307 } 308 309 float stackCenter = mStackPosition.x + mBubbleSize / 2; 310 float screenCenter = mLayout.getWidth() / 2; 311 return stackCenter < screenCenter; 312 } 313 314 /** 315 * Fling stack to given corner, within allowable screen bounds. 316 * Note that we need new SpringForce instances per animation despite identical configs because 317 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs. 318 */ springStack( float destinationX, float destinationY, float stiffness)319 public void springStack( 320 float destinationX, float destinationY, float stiffness) { 321 notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY); 322 323 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, 324 new SpringForce() 325 .setStiffness(stiffness) 326 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 327 0 /* startXVelocity */, 328 destinationX); 329 330 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, 331 new SpringForce() 332 .setStiffness(stiffness) 333 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 334 0 /* startYVelocity */, 335 destinationY); 336 } 337 338 /** 339 * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after 340 * flings. 341 */ springStackAfterFling(float destinationX, float destinationY)342 public void springStackAfterFling(float destinationX, float destinationY) { 343 springStack(destinationX, destinationY, STACK_SPRING_STIFFNESS); 344 } 345 346 /** 347 * Flings the stack starting with the given velocities, springing it to the nearest edge 348 * afterward. 349 * 350 * @return The X value that the stack will end up at after the fling/spring. 351 */ flingStackThenSpringToEdge(float x, float velX, float velY)352 public float flingStackThenSpringToEdge(float x, float velX, float velY) { 353 final boolean stackOnLeftSide = x - mBubbleSize / 2 < mLayout.getWidth() / 2; 354 355 final boolean stackShouldFlingLeft = stackOnLeftSide 356 ? velX < ESCAPE_VELOCITY 357 : velX < -ESCAPE_VELOCITY; 358 359 final RectF stackBounds = getAllowableStackPositionRegion(); 360 361 // Target X translation (either the left or right side of the screen). 362 final float destinationRelativeX = stackShouldFlingLeft 363 ? stackBounds.left : stackBounds.right; 364 365 // If all bubbles were removed during a drag event, just return the X we would have animated 366 // to if there were still bubbles. 367 if (mLayout == null || mLayout.getChildCount() == 0) { 368 return destinationRelativeX; 369 } 370 371 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 372 final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", 373 STACK_SPRING_STIFFNESS /* default */); 374 final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", 375 SPRING_AFTER_FLING_DAMPING_RATIO); 376 final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction", 377 FLING_FRICTION); 378 379 // Minimum velocity required for the stack to make it to the targeted side of the screen, 380 // taking friction into account (4.2f is the number that friction scalars are multiplied by 381 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, 382 // but the SpringAnimation at the end will ensure that it reaches the destination X 383 // regardless. 384 final float minimumVelocityToReachEdge = 385 (destinationRelativeX - x) * (friction * 4.2f); 386 387 final float estimatedY = PhysicsAnimator.estimateFlingEndValue( 388 mStackPosition.y, velY, 389 new PhysicsAnimator.FlingConfig( 390 friction, stackBounds.top, stackBounds.bottom)); 391 392 notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY); 393 394 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so 395 // that it'll make it all the way to the side of the screen. 396 final float startXVelocity = stackShouldFlingLeft 397 ? Math.min(minimumVelocityToReachEdge, velX) 398 : Math.max(minimumVelocityToReachEdge, velX); 399 400 401 402 flingThenSpringFirstBubbleWithStackFollowing( 403 DynamicAnimation.TRANSLATION_X, 404 startXVelocity, 405 friction, 406 new SpringForce() 407 .setStiffness(stiffness) 408 .setDampingRatio(dampingRatio), 409 destinationRelativeX); 410 411 flingThenSpringFirstBubbleWithStackFollowing( 412 DynamicAnimation.TRANSLATION_Y, 413 velY, 414 friction, 415 new SpringForce() 416 .setStiffness(stiffness) 417 .setDampingRatio(dampingRatio), 418 /* destination */ null); 419 420 // If we're flinging now, there's no more touch event to catch up to. 421 mFirstBubbleSpringingToTouch = false; 422 mIsMovingFromFlinging = true; 423 return destinationRelativeX; 424 } 425 426 /** 427 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). 428 */ 429 public PointF getStackPositionAlongNearestHorizontalEdge() { 430 if (mPositioner.showingInTaskbar()) { 431 return mPositioner.getRestingPosition(); 432 } 433 final PointF stackPos = getStackPosition(); 434 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); 435 final RectF bounds = getAllowableStackPositionRegion(); 436 437 stackPos.x = onLeft ? bounds.left : bounds.right; 438 return stackPos; 439 } 440 441 /** Description of current animation controller state. */ 442 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 443 pw.println("StackAnimationController state:"); 444 pw.print(" isActive: "); pw.println(isActiveController()); 445 pw.print(" restingStackPos: "); 446 pw.println(mPositioner.getRestingPosition().toString()); 447 pw.print(" currentStackPos: "); pw.println(mStackPosition.toString()); 448 pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging); 449 pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget()); 450 pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch); 451 } 452 453 /** 454 * Flings the first bubble along the given property's axis, using the provided configuration 455 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently 456 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final 457 * position. 458 */ 459 protected void flingThenSpringFirstBubbleWithStackFollowing( 460 DynamicAnimation.ViewProperty property, 461 float vel, 462 float friction, 463 SpringForce spring, 464 Float finalPosition) { 465 if (!isActiveController()) { 466 return; 467 } 468 469 Log.d(TAG, String.format("Flinging %s.", 470 PhysicsAnimationLayout.getReadablePropertyName(property))); 471 472 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 473 final float currentValue = firstBubbleProperty.getValue(this); 474 final RectF bounds = getAllowableStackPositionRegion(); 475 final float min = 476 property.equals(DynamicAnimation.TRANSLATION_X) 477 ? bounds.left 478 : bounds.top; 479 final float max = 480 property.equals(DynamicAnimation.TRANSLATION_X) 481 ? bounds.right 482 : bounds.bottom; 483 484 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty); 485 flingAnimation.setFriction(friction) 486 .setStartVelocity(vel) 487 488 // If the bubble's property value starts beyond the desired min/max, use that value 489 // instead so that the animation won't immediately end. If, for example, the user 490 // drags the bubbles into the navigation bar, but then flings them upward, we want 491 // the fling to occur despite temporarily having a value outside of the min/max. If 492 // the bubbles are out of bounds and flung even farther out of bounds, the fling 493 // animation will halt immediately and the SpringAnimation will take over, springing 494 // it in reverse to the (legal) final position. 495 .setMinValue(Math.min(currentValue, min)) 496 .setMaxValue(Math.max(currentValue, max)) 497 498 .addEndListener((animation, canceled, endValue, endVelocity) -> { 499 if (!canceled) { 500 mPositioner.setRestingPosition(mStackPosition); 501 502 springFirstBubbleWithStackFollowing(property, spring, endVelocity, 503 finalPosition != null 504 ? finalPosition 505 : Math.max(min, Math.min(max, endValue))); 506 } 507 }); 508 509 cancelStackPositionAnimation(property); 510 mStackPositionAnimations.put(property, flingAnimation); 511 flingAnimation.start(); 512 } 513 514 /** 515 * Cancel any stack position animations that were started by calling 516 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end 517 * listeners. 518 */ 519 public void cancelStackPositionAnimations() { 520 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); 521 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); 522 523 removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); 524 removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); 525 } 526 527 /** Save the current IME height so that we know where the stack bounds should be. */ 528 public void setImeHeight(int imeHeight) { 529 mImeHeight = imeHeight; 530 } 531 532 /** Returns the current IME height that the stack is offset by. */ 533 public float getImeHeight() { 534 return mImeHeight; 535 } 536 537 /** 538 * Animates the stack either away from the newly visible IME, or back to its original position 539 * due to the IME going away. 540 * 541 * @return The destination Y value of the stack due to the IME movement (or the current position 542 * of the stack if it's not moving). 543 */ 544 public float animateForImeVisibility(boolean imeVisible) { 545 final float maxBubbleY = getAllowableStackPositionRegion().bottom; 546 float destinationY = UNSET; 547 548 if (imeVisible) { 549 // Stack is lower than it should be and overlaps the now-visible IME. 550 if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) { 551 mPreImeY = mStackPosition.y; 552 destinationY = maxBubbleY; 553 } 554 } else { 555 if (mPreImeY != UNSET) { 556 destinationY = mPreImeY; 557 mPreImeY = UNSET; 558 } 559 } 560 561 if (destinationY != UNSET) { 562 springFirstBubbleWithStackFollowing( 563 DynamicAnimation.TRANSLATION_Y, 564 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null) 565 .setStiffness(IME_ANIMATION_STIFFNESS), 566 /* startVel */ 0f, 567 destinationY); 568 569 notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY); 570 } 571 572 return destinationY != UNSET ? destinationY : mStackPosition.y; 573 } 574 575 /** 576 * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so 577 * we return these bounds from 578 * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. 579 */ 580 private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) { 581 final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen(); 582 floatingBounds.offsetTo((int) x, (int) y); 583 mAnimatingToBounds = floatingBounds; 584 mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); 585 } 586 587 /** 588 * Returns the region that the stack position must stay within. This goes slightly off the left 589 * and right sides of the screen, below the status bar/cutout and above the navigation bar. 590 * While the stack position is not allowed to rest outside of these bounds, it can temporarily 591 * be animated or dragged beyond them. 592 */ 593 public RectF getAllowableStackPositionRegion() { 594 final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); 595 allowableRegion.left -= mBubbleOffscreen; 596 allowableRegion.top += mBubblePaddingTop; 597 allowableRegion.right += mBubbleOffscreen - mBubbleSize; 598 allowableRegion.bottom -= mBubblePaddingTop + mBubbleSize 599 + (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f); 600 return allowableRegion; 601 } 602 603 /** Moves the stack in response to a touch event. */ 604 public void moveStackFromTouch(float x, float y) { 605 // Begin the spring-to-touch catch up animation if needed. 606 if (mSpringToTouchOnNextMotionEvent) { 607 springStack(x, y, SPRING_TO_TOUCH_STIFFNESS); 608 mSpringToTouchOnNextMotionEvent = false; 609 mFirstBubbleSpringingToTouch = true; 610 } else if (mFirstBubbleSpringingToTouch) { 611 final SpringAnimation springToTouchX = 612 (SpringAnimation) mStackPositionAnimations.get( 613 DynamicAnimation.TRANSLATION_X); 614 final SpringAnimation springToTouchY = 615 (SpringAnimation) mStackPositionAnimations.get( 616 DynamicAnimation.TRANSLATION_Y); 617 618 // If either animation is still running, we haven't caught up. Update the animations. 619 if (springToTouchX.isRunning() || springToTouchY.isRunning()) { 620 springToTouchX.animateToFinalPosition(x); 621 springToTouchY.animateToFinalPosition(y); 622 } else { 623 // If the animations have finished, the stack is now at the touch point. We can 624 // resume moving the bubble directly. 625 mFirstBubbleSpringingToTouch = false; 626 } 627 } 628 629 if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) { 630 moveFirstBubbleWithStackFollowing(x, y); 631 } 632 } 633 634 /** Notify the controller that the stack has been unstuck from the dismiss target. */ 635 public void onUnstuckFromTarget() { 636 mSpringToTouchOnNextMotionEvent = true; 637 } 638 639 /** 640 * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down. 641 */ 642 public void animateStackDismissal(float translationYBy, Runnable after) { 643 animationsForChildrenFromIndex(0, (index, animation) -> 644 animation 645 .scaleX(0f) 646 .scaleY(0f) 647 .alpha(0f) 648 .translationY( 649 mLayout.getChildAt(index).getTranslationY() + translationYBy) 650 .withStiffness(SpringForce.STIFFNESS_HIGH)) 651 .startAll(after); 652 } 653 654 /** 655 * Springs the first bubble to the given final position, with the rest of the stack 'following'. 656 */ springFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, SpringForce spring, float vel, float finalPosition, @Nullable Runnable... after)657 protected void springFirstBubbleWithStackFollowing( 658 DynamicAnimation.ViewProperty property, SpringForce spring, 659 float vel, float finalPosition, @Nullable Runnable... after) { 660 661 if (mLayout.getChildCount() == 0 || !isActiveController()) { 662 return; 663 } 664 665 Log.d(TAG, String.format("Springing %s to final position %f.", 666 PhysicsAnimationLayout.getReadablePropertyName(property), 667 finalPosition)); 668 669 // Whether we're springing towards the touch location, rather than to a position on the 670 // sides of the screen. 671 final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent; 672 673 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 674 SpringAnimation springAnimation = 675 new SpringAnimation(this, firstBubbleProperty) 676 .setSpring(spring) 677 .addEndListener((dynamicAnimation, b, v, v1) -> { 678 if (!isSpringingTowardsTouch) { 679 // If we're springing towards the touch position, don't save the 680 // resting position - the touch location is not a valid resting 681 // position. We'll set this when the stack springs to the left or 682 // right side of the screen after the touch gesture ends. 683 mPositioner.setRestingPosition(mStackPosition); 684 } 685 686 if (mOnStackAnimationFinished != null) { 687 mOnStackAnimationFinished.run(); 688 } 689 690 if (after != null) { 691 for (Runnable callback : after) { 692 callback.run(); 693 } 694 } 695 }) 696 .setStartVelocity(vel); 697 698 cancelStackPositionAnimation(property); 699 mStackPositionAnimations.put(property, springAnimation); 700 springAnimation.animateToFinalPosition(finalPosition); 701 } 702 703 @Override getAnimatedProperties()704 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 705 return Sets.newHashSet( 706 DynamicAnimation.TRANSLATION_X, // For positioning. 707 DynamicAnimation.TRANSLATION_Y, 708 DynamicAnimation.ALPHA, // For fading in new bubbles. 709 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles. 710 DynamicAnimation.SCALE_Y); 711 } 712 713 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)714 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 715 if (property.equals(DynamicAnimation.TRANSLATION_X) 716 || property.equals(DynamicAnimation.TRANSLATION_Y)) { 717 return index + 1; 718 } else { 719 return NONE; 720 } 721 } 722 723 724 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)725 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index) { 726 if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 727 // If we're in the dismiss target, have the bubbles pile on top of each other with no 728 // offset. 729 if (isStackStuckToTarget()) { 730 return 0f; 731 } else { 732 // We only show the first two bubbles in the stack & the rest hide behind them 733 // so they don't need an offset. 734 return index > (NUM_VISIBLE_WHEN_RESTING - 1) ? 0f : mStackOffset; 735 } 736 } else { 737 return 0f; 738 } 739 } 740 741 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)742 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 743 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 744 final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", 745 DEFAULT_BOUNCINESS); 746 747 return new SpringForce() 748 .setDampingRatio(dampingRatio) 749 .setStiffness(CHAIN_STIFFNESS); 750 } 751 752 @Override onChildAdded(View child, int index)753 void onChildAdded(View child, int index) { 754 // Don't animate additions within the dismiss target. 755 if (isStackStuckToTarget()) { 756 return; 757 } 758 759 if (getBubbleCount() == 1) { 760 // If this is the first child added, position the stack in its starting position. 761 moveStackToStartPosition(); 762 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { 763 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble 764 // to the back of the stack, it'll be largely invisible so don't bother animating it in. 765 animateInBubble(child, index); 766 } 767 } 768 769 @Override onChildRemoved(View child, int index, Runnable finishRemoval)770 void onChildRemoved(View child, int index, Runnable finishRemoval) { 771 PhysicsAnimator.getInstance(child) 772 .spring(DynamicAnimation.ALPHA, 0f) 773 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) 774 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) 775 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) 776 .start(); 777 778 // If there are other bubbles, pull them into the correct position. 779 if (getBubbleCount() > 0) { 780 animationForChildAtIndex(0).translationX(mStackPosition.x).start(); 781 } else { 782 // When all children are removed ensure stack position is sane 783 mPositioner.setRestingPosition(mPositioner.getRestingPosition()); 784 785 // Remove the stack from the coordinator since we don't have any bubbles and aren't 786 // visible. 787 mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent); 788 } 789 } 790 animateReorder(List<View> bubbleViews, Runnable after)791 public void animateReorder(List<View> bubbleViews, Runnable after) { 792 // After the bubble going to index 0 springs above stack, update all icons 793 // at the same time, to avoid visibly changing bubble order before the animation completes. 794 Runnable updateAllIcons = () -> { 795 for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { 796 View view = bubbleViews.get(newIndex); 797 updateBadgesAndZOrder(view, newIndex); 798 } 799 }; 800 801 for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { 802 View view = bubbleViews.get(newIndex); 803 final int oldIndex = mLayout.indexOfChild(view); 804 animateSwap(view, oldIndex, newIndex, updateAllIcons, after); 805 } 806 } 807 animateSwap(View view, int oldIndex, int newIndex, Runnable updateAllIcons, Runnable finishReorder)808 private void animateSwap(View view, int oldIndex, int newIndex, 809 Runnable updateAllIcons, Runnable finishReorder) { 810 if (newIndex == oldIndex) { 811 // Add new bubble to index 0; move existing bubbles down 812 updateBadgesAndZOrder(view, newIndex); 813 if (newIndex == 0) { 814 animateInBubble(view, newIndex); 815 } else { 816 moveToFinalIndex(view, newIndex, finishReorder); 817 } 818 } else { 819 // Reorder existing bubbles 820 if (newIndex == 0) { 821 animateToFrontThenUpdateIcons(view, updateAllIcons, finishReorder); 822 } else { 823 moveToFinalIndex(view, newIndex, finishReorder); 824 } 825 } 826 } 827 animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons, Runnable finishReorder)828 private void animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons, 829 Runnable finishReorder) { 830 final ViewPropertyAnimator animator = v.animate() 831 .translationY(getStackPosition().y - mSwapAnimationOffset) 832 .setDuration(BUBBLE_SWAP_DURATION) 833 .withEndAction(() -> { 834 updateAllIcons.run(); 835 moveToFinalIndex(v, 0 /* index */, finishReorder); 836 }); 837 v.setTag(R.id.reorder_animator_tag, animator); 838 } 839 moveToFinalIndex(View view, int newIndex, Runnable finishReorder)840 private void moveToFinalIndex(View view, int newIndex, 841 Runnable finishReorder) { 842 final ViewPropertyAnimator animator = view.animate() 843 .translationY(getStackPosition().y 844 + Math.min(newIndex, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffset) 845 .setDuration(BUBBLE_SWAP_DURATION) 846 .withEndAction(() -> { 847 view.setTag(R.id.reorder_animator_tag, null); 848 finishReorder.run(); 849 }); 850 view.setTag(R.id.reorder_animator_tag, animator); 851 } 852 853 // TODO: do we need this & BubbleStackView#updateBadgesAndZOrder? updateBadgesAndZOrder(View v, int index)854 private void updateBadgesAndZOrder(View v, int index) { 855 v.setZ(index < NUM_VISIBLE_WHEN_RESTING ? (mMaxBubbles * mElevation) - index : 0f); 856 BadgedImageView bv = (BadgedImageView) v; 857 if (index == 0) { 858 bv.showDotAndBadge(!isStackOnLeftSide()); 859 } else { 860 bv.hideDotAndBadge(!isStackOnLeftSide()); 861 } 862 } 863 864 @Override 865 void onChildReordered(View child, int oldIndex, int newIndex) {} 866 867 @Override 868 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 869 Resources res = layout.getResources(); 870 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 871 mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset); 872 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); 873 mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 874 mBubbleSize = mPositioner.getBubbleSize(); 875 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 876 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); 877 } 878 879 /** 880 * Update resources. 881 */ 882 public void updateResources() { 883 if (mLayout != null) { 884 Resources res = mLayout.getContext().getResources(); 885 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 886 } 887 } 888 889 private boolean isStackStuckToTarget() { 890 return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget(); 891 } 892 893 /** Moves the stack, without any animation, to the starting position. */ 894 private void moveStackToStartPosition() { 895 // Post to ensure that the layout's width and height have been calculated. 896 mLayout.setVisibility(View.INVISIBLE); 897 mLayout.post(() -> { 898 setStackPosition(mPositioner.getRestingPosition()); 899 900 mStackMovedToStartPosition = true; 901 mLayout.setVisibility(View.VISIBLE); 902 903 // Animate in the top bubble now that we're visible. 904 if (mLayout.getChildCount() > 0) { 905 // Add the stack to the floating content coordinator now that we have a bubble and 906 // are visible. 907 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent); 908 909 animateInBubble(mLayout.getChildAt(0), 0 /* index */); 910 } 911 }); 912 } 913 914 /** 915 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent 916 * bubbles to animate 'following' to the new location. 917 */ moveFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, float value)918 private void moveFirstBubbleWithStackFollowing( 919 DynamicAnimation.ViewProperty property, float value) { 920 921 // Update the canonical stack position. 922 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 923 mStackPosition.x = value; 924 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 925 mStackPosition.y = value; 926 } 927 928 if (mLayout.getChildCount() > 0) { 929 property.setValue(mLayout.getChildAt(0), value); 930 if (mLayout.getChildCount() > 1) { 931 float newValue = value + getOffsetForChainedPropertyAnimation(property, 0); 932 animationForChildAtIndex(1) 933 .property(property, newValue) 934 .start(); 935 } 936 } 937 } 938 939 /** Moves the stack to a position instantly, with no animation. */ setStackPosition(PointF pos)940 public void setStackPosition(PointF pos) { 941 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); 942 mStackPosition.set(pos.x, pos.y); 943 944 mPositioner.setRestingPosition(mStackPosition); 945 946 // If we're not the active controller, we don't want to physically move the bubble views. 947 if (isActiveController()) { 948 // Cancel animations that could be moving the views. 949 mLayout.cancelAllAnimationsOfProperties( 950 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); 951 cancelStackPositionAnimations(); 952 953 // Since we're not using the chained animations, apply the offsets manually. 954 final float xOffset = getOffsetForChainedPropertyAnimation( 955 DynamicAnimation.TRANSLATION_X, 0); 956 final float yOffset = getOffsetForChainedPropertyAnimation( 957 DynamicAnimation.TRANSLATION_Y, 0); 958 for (int i = 0; i < mLayout.getChildCount(); i++) { 959 float index = Math.min(i, NUM_VISIBLE_WHEN_RESTING - 1); 960 mLayout.getChildAt(i).setTranslationX(pos.x + (index * xOffset)); 961 mLayout.getChildAt(i).setTranslationY(pos.y + (index * yOffset)); 962 } 963 } 964 } 965 setStackPosition(BubbleStackView.RelativeStackPosition position)966 public void setStackPosition(BubbleStackView.RelativeStackPosition position) { 967 setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); 968 } 969 isStackPositionSet()970 private boolean isStackPositionSet() { 971 return mStackMovedToStartPosition; 972 } 973 974 /** Animates in the given bubble. */ animateInBubble(View v, int index)975 private void animateInBubble(View v, int index) { 976 if (!isActiveController()) { 977 return; 978 } 979 980 final float yOffset = 981 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y, 0); 982 float endY = mStackPosition.y + yOffset * index; 983 float endX = mStackPosition.x; 984 if (mPositioner.showBubblesVertically()) { 985 v.setTranslationY(endY); 986 final float startX = isStackOnLeftSide() 987 ? endX - NEW_BUBBLE_START_Y 988 : endX + NEW_BUBBLE_START_Y; 989 v.setTranslationX(startX); 990 } else { 991 v.setTranslationX(mStackPosition.x); 992 final float startY = endY + NEW_BUBBLE_START_Y; 993 v.setTranslationY(startY); 994 } 995 v.setScaleX(NEW_BUBBLE_START_SCALE); 996 v.setScaleY(NEW_BUBBLE_START_SCALE); 997 v.setAlpha(0f); 998 final ViewPropertyAnimator animator = v.animate() 999 .scaleX(1f) 1000 .scaleY(1f) 1001 .alpha(1f) 1002 .setDuration(BUBBLE_SWAP_DURATION) 1003 .withEndAction(() -> { 1004 v.setTag(R.id.reorder_animator_tag, null); 1005 }); 1006 v.setTag(R.id.reorder_animator_tag, animator); 1007 if (mPositioner.showBubblesVertically()) { 1008 animator.translationX(endX); 1009 } else { 1010 animator.translationY(endY); 1011 } 1012 } 1013 1014 /** 1015 * Cancels any outstanding first bubble property animations that are running. This does not 1016 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only 1017 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and 1018 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. 1019 */ cancelStackPositionAnimation(DynamicAnimation.ViewProperty property)1020 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) { 1021 if (mStackPositionAnimations.containsKey(property)) { 1022 mStackPositionAnimations.get(property).cancel(); 1023 } 1024 } 1025 1026 /** 1027 * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided 1028 * {@link MagnetizedObject.MagneticTarget} added as a target. 1029 */ getMagnetizedStack( MagnetizedObject.MagneticTarget target)1030 public MagnetizedObject<StackAnimationController> getMagnetizedStack( 1031 MagnetizedObject.MagneticTarget target) { 1032 if (mMagnetizedStack == null) { 1033 mMagnetizedStack = new MagnetizedObject<StackAnimationController>( 1034 mLayout.getContext(), 1035 this, 1036 new StackPositionProperty(DynamicAnimation.TRANSLATION_X), 1037 new StackPositionProperty(DynamicAnimation.TRANSLATION_Y) 1038 ) { 1039 @Override 1040 public float getWidth(@NonNull StackAnimationController underlyingObject) { 1041 return mBubbleSize; 1042 } 1043 1044 @Override 1045 public float getHeight(@NonNull StackAnimationController underlyingObject) { 1046 return mBubbleSize; 1047 } 1048 1049 @Override 1050 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject, 1051 @NonNull int[] loc) { 1052 loc[0] = (int) mStackPosition.x; 1053 loc[1] = (int) mStackPosition.y; 1054 } 1055 }; 1056 mMagnetizedStack.addTarget(target); 1057 mMagnetizedStack.setHapticsEnabled(true); 1058 mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); 1059 } 1060 1061 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 1062 final float minVelocity = Settings.Secure.getFloat(contentResolver, 1063 "bubble_dismiss_fling_min_velocity", 1064 mMagnetizedStack.getFlingToTargetMinVelocity() /* default */); 1065 final float maxVelocity = Settings.Secure.getFloat(contentResolver, 1066 "bubble_dismiss_stick_max_velocity", 1067 mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */); 1068 final float targetWidth = Settings.Secure.getFloat(contentResolver, 1069 "bubble_dismiss_target_width_percent", 1070 mMagnetizedStack.getFlingToTargetWidthPercent() /* default */); 1071 1072 mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity); 1073 mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity); 1074 mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth); 1075 1076 return mMagnetizedStack; 1077 } 1078 1079 /** Returns the number of 'real' bubbles (excluding overflow). */ getBubbleCount()1080 private int getBubbleCount() { 1081 return mBubbleCountSupplier.getAsInt(); 1082 } 1083 1084 /** 1085 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's 1086 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this 1087 * property directly to move the first bubble and cause the stack to 'follow' to the new 1088 * location. 1089 * 1090 * This could also be achieved by simply animating the first bubble view and adding an update 1091 * listener to dispatch movement to the rest of the stack. However, this would require 1092 * duplication of logic in that update handler - it's simpler to keep all logic contained in the 1093 * {@link #moveFirstBubbleWithStackFollowing} method. 1094 */ 1095 private class StackPositionProperty 1096 extends FloatPropertyCompat<StackAnimationController> { 1097 private final DynamicAnimation.ViewProperty mProperty; 1098 StackPositionProperty(DynamicAnimation.ViewProperty property)1099 private StackPositionProperty(DynamicAnimation.ViewProperty property) { 1100 super(property.toString()); 1101 mProperty = property; 1102 } 1103 1104 @Override getValue(StackAnimationController controller)1105 public float getValue(StackAnimationController controller) { 1106 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0; 1107 } 1108 1109 @Override setValue(StackAnimationController controller, float value)1110 public void setValue(StackAnimationController controller, float value) { 1111 moveFirstBubbleWithStackFollowing(mProperty, value); 1112 } 1113 } 1114 } 1115 1116