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