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