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