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.systemui.bubbles.animation; 18 19 import android.content.res.Resources; 20 import android.graphics.PointF; 21 import android.graphics.RectF; 22 import android.util.Log; 23 import android.view.View; 24 import android.view.WindowInsets; 25 26 import androidx.dynamicanimation.animation.DynamicAnimation; 27 import androidx.dynamicanimation.animation.FlingAnimation; 28 import androidx.dynamicanimation.animation.FloatPropertyCompat; 29 import androidx.dynamicanimation.animation.SpringAnimation; 30 import androidx.dynamicanimation.animation.SpringForce; 31 32 import com.android.systemui.R; 33 34 import com.google.android.collect.Sets; 35 36 import java.util.HashMap; 37 import java.util.Set; 38 39 /** 40 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop 41 * each other with a slight offset to the left or right (depending on which side of the screen they 42 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of 43 * the screen. 44 */ 45 public class StackAnimationController extends 46 PhysicsAnimationLayout.PhysicsAnimationController { 47 48 private static final String TAG = "Bubbs.StackCtrl"; 49 50 /** Scale factor to use initially for new bubbles being animated in. */ 51 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f; 52 53 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */ 54 private static final int ANIMATE_TRANSLATION_FACTOR = 4; 55 56 /** 57 * Values to use for the default {@link SpringForce} provided to the physics animation layout. 58 */ 59 private static final int DEFAULT_STIFFNESS = 12000; 60 private static final int FLING_FOLLOW_STIFFNESS = 20000; 61 private static final float DEFAULT_BOUNCINESS = 0.9f; 62 63 /** 64 * Friction applied to fling animations. Since the stack must land on one of the sides of the 65 * screen, we want less friction horizontally so that the stack has a better chance of making it 66 * to the side without needing a spring. 67 */ 68 private static final float FLING_FRICTION_X = 2.2f; 69 private static final float FLING_FRICTION_Y = 2.2f; 70 71 /** 72 * Values to use for the stack spring animation used to spring the stack to its final position 73 * after a fling. 74 */ 75 private static final int SPRING_AFTER_FLING_STIFFNESS = 750; 76 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; 77 78 /** 79 * Minimum fling velocity required to trigger moving the stack from one side of the screen to 80 * the other. 81 */ 82 private static final float ESCAPE_VELOCITY = 750f; 83 84 /** 85 * The canonical position of the stack. This is typically the position of the first bubble, but 86 * we need to keep track of it separately from the first bubble's translation in case there are 87 * no bubbles, or the first bubble was just added and being animated to its new position. 88 */ 89 private PointF mStackPosition = new PointF(-1, -1); 90 91 /** Whether or not the stack's start position has been set. */ 92 private boolean mStackMovedToStartPosition = false; 93 94 /** The most recent position in which the stack was resting on the edge of the screen. */ 95 private PointF mRestingStackPosition; 96 97 /** The height of the most recently visible IME. */ 98 private float mImeHeight = 0f; 99 100 /** 101 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the 102 * IME is not visible or the user moved the stack since the IME became visible. 103 */ 104 private float mPreImeY = Float.MIN_VALUE; 105 106 /** 107 * Animations on the stack position itself, which would have been started in 108 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to 109 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect) 110 * to a legal position on the side of the screen. 111 */ 112 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations = 113 new HashMap<>(); 114 115 /** 116 * Whether the current motion of the stack is due to a fling animation (vs. being dragged 117 * manually). 118 */ 119 private boolean mIsMovingFromFlinging = false; 120 121 /** 122 * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung). 123 */ 124 private boolean mWithinDismissTarget = false; 125 126 /** 127 * Whether the first bubble is springing towards the touch point, rather than using the default 128 * behavior of moving directly to the touch point with the rest of the stack following it. 129 * 130 * This happens when the user's finger exits the dismiss area while the stack is magnetized to 131 * the center. Since the touch point differs from the stack location, we need to animate the 132 * stack back to the touch point to avoid a jarring instant location change from the center of 133 * the target to the touch point just outside the target bounds. 134 * 135 * This is reset once the spring animations end, since that means the first bubble has 136 * successfully 'caught up' to the touch. 137 */ 138 private boolean mFirstBubbleSpringingToTouch = false; 139 140 /** Horizontal offset of bubbles in the stack. */ 141 private float mStackOffset; 142 /** Diameter of the bubbles themselves. */ 143 private int mIndividualBubbleSize; 144 /** 145 * The amount of space to add between the bubbles and certain UI elements, such as the top of 146 * the screen or the IME. This does not apply to the left/right sides of the screen since the 147 * stack goes offscreen intentionally. 148 */ 149 private int mBubblePadding; 150 /** How far offscreen the stack rests. */ 151 private int mBubbleOffscreen; 152 /** How far down the screen the stack starts, when there is no pre-existing location. */ 153 private int mStackStartingVerticalOffset; 154 /** Height of the status bar. */ 155 private float mStatusBarHeight; 156 157 /** 158 * Instantly move the first bubble to the given point, and animate the rest of the stack behind 159 * it with the 'following' effect. 160 */ moveFirstBubbleWithStackFollowing(float x, float y)161 public void moveFirstBubbleWithStackFollowing(float x, float y) { 162 // If we manually move the bubbles with the IME open, clear the return point since we don't 163 // want the stack to snap away from the new position. 164 mPreImeY = Float.MIN_VALUE; 165 166 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x); 167 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y); 168 169 // This method is called when the stack is being dragged manually, so we're clearly no 170 // longer flinging. 171 mIsMovingFromFlinging = false; 172 } 173 174 /** 175 * The position of the stack - typically the position of the first bubble; if no bubbles have 176 * been added yet, it will be where the first bubble will go when added. 177 */ getStackPosition()178 public PointF getStackPosition() { 179 return mStackPosition; 180 } 181 182 /** Whether the stack is on the left side of the screen. */ isStackOnLeftSide()183 public boolean isStackOnLeftSide() { 184 if (mLayout == null || !isStackPositionSet()) { 185 return false; 186 } 187 188 float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2; 189 float screenCenter = mLayout.getWidth() / 2; 190 return stackCenter < screenCenter; 191 } 192 193 /** 194 * Fling stack to given corner, within allowable screen bounds. 195 * Note that we need new SpringForce instances per animation despite identical configs because 196 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs. 197 */ springStack(float destinationX, float destinationY)198 public void springStack(float destinationX, float destinationY) { 199 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, 200 new SpringForce() 201 .setStiffness(SPRING_AFTER_FLING_STIFFNESS) 202 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 203 0 /* startXVelocity */, 204 destinationX); 205 206 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, 207 new SpringForce() 208 .setStiffness(SPRING_AFTER_FLING_STIFFNESS) 209 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 210 0 /* startYVelocity */, 211 destinationY); 212 } 213 214 /** 215 * Flings the stack starting with the given velocities, springing it to the nearest edge 216 * afterward. 217 * 218 * @return The X value that the stack will end up at after the fling/spring. 219 */ flingStackThenSpringToEdge(float x, float velX, float velY)220 public float flingStackThenSpringToEdge(float x, float velX, float velY) { 221 final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2; 222 223 final boolean stackShouldFlingLeft = stackOnLeftSide 224 ? velX < ESCAPE_VELOCITY 225 : velX < -ESCAPE_VELOCITY; 226 227 final RectF stackBounds = getAllowableStackPositionRegion(); 228 229 // Target X translation (either the left or right side of the screen). 230 final float destinationRelativeX = stackShouldFlingLeft 231 ? stackBounds.left : stackBounds.right; 232 233 // Minimum velocity required for the stack to make it to the targeted side of the screen, 234 // taking friction into account (4.2f is the number that friction scalars are multiplied by 235 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, 236 // but the SpringAnimation at the end will ensure that it reaches the destination X 237 // regardless. 238 final float minimumVelocityToReachEdge = 239 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f); 240 241 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so 242 // that it'll make it all the way to the side of the screen. 243 final float startXVelocity = stackShouldFlingLeft 244 ? Math.min(minimumVelocityToReachEdge, velX) 245 : Math.max(minimumVelocityToReachEdge, velX); 246 247 flingThenSpringFirstBubbleWithStackFollowing( 248 DynamicAnimation.TRANSLATION_X, 249 startXVelocity, 250 FLING_FRICTION_X, 251 new SpringForce() 252 .setStiffness(SPRING_AFTER_FLING_STIFFNESS) 253 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 254 destinationRelativeX); 255 256 flingThenSpringFirstBubbleWithStackFollowing( 257 DynamicAnimation.TRANSLATION_Y, 258 velY, 259 FLING_FRICTION_Y, 260 new SpringForce() 261 .setStiffness(SPRING_AFTER_FLING_STIFFNESS) 262 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 263 /* destination */ null); 264 265 mLayout.setEndActionForMultipleProperties( 266 () -> { 267 mRestingStackPosition = new PointF(); 268 mRestingStackPosition.set(mStackPosition); 269 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); 270 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); 271 }, 272 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); 273 274 // If we're flinging now, there's no more touch event to catch up to. 275 mFirstBubbleSpringingToTouch = false; 276 mIsMovingFromFlinging = true; 277 return destinationRelativeX; 278 } 279 280 /** 281 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). 282 */ 283 public PointF getStackPositionAlongNearestHorizontalEdge() { 284 final PointF stackPos = getStackPosition(); 285 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); 286 final RectF bounds = getAllowableStackPositionRegion(); 287 288 stackPos.x = onLeft ? bounds.left : bounds.right; 289 return stackPos; 290 } 291 292 /** 293 * Moves the stack in response to rotation. We keep it in the most similar position by keeping 294 * it on the same side, and positioning it the same percentage of the way down the screen 295 * (taking status bar/nav bar into account by using the allowable region's height). 296 */ 297 public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) { 298 final RectF allowablePos = getAllowableStackPositionRegion(); 299 final float allowableRegionHeight = allowablePos.bottom - allowablePos.top; 300 301 final float x = wasOnLeft ? allowablePos.left : allowablePos.right; 302 final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top; 303 304 setStackPosition(new PointF(x, y)); 305 } 306 307 /** 308 * Flings the first bubble along the given property's axis, using the provided configuration 309 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently 310 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final 311 * position. 312 */ 313 protected void flingThenSpringFirstBubbleWithStackFollowing( 314 DynamicAnimation.ViewProperty property, 315 float vel, 316 float friction, 317 SpringForce spring, 318 Float finalPosition) { 319 Log.d(TAG, String.format("Flinging %s.", 320 PhysicsAnimationLayout.getReadablePropertyName(property))); 321 322 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 323 final float currentValue = firstBubbleProperty.getValue(this); 324 final RectF bounds = getAllowableStackPositionRegion(); 325 final float min = 326 property.equals(DynamicAnimation.TRANSLATION_X) 327 ? bounds.left 328 : bounds.top; 329 final float max = 330 property.equals(DynamicAnimation.TRANSLATION_X) 331 ? bounds.right 332 : bounds.bottom; 333 334 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty); 335 flingAnimation.setFriction(friction) 336 .setStartVelocity(vel) 337 338 // If the bubble's property value starts beyond the desired min/max, use that value 339 // instead so that the animation won't immediately end. If, for example, the user 340 // drags the bubbles into the navigation bar, but then flings them upward, we want 341 // the fling to occur despite temporarily having a value outside of the min/max. If 342 // the bubbles are out of bounds and flung even farther out of bounds, the fling 343 // animation will halt immediately and the SpringAnimation will take over, springing 344 // it in reverse to the (legal) final position. 345 .setMinValue(Math.min(currentValue, min)) 346 .setMaxValue(Math.max(currentValue, max)) 347 348 .addEndListener((animation, canceled, endValue, endVelocity) -> { 349 if (!canceled) { 350 springFirstBubbleWithStackFollowing(property, spring, endVelocity, 351 finalPosition != null 352 ? finalPosition 353 : Math.max(min, Math.min(max, endValue))); 354 } 355 }); 356 357 cancelStackPositionAnimation(property); 358 mStackPositionAnimations.put(property, flingAnimation); 359 flingAnimation.start(); 360 } 361 362 /** 363 * Cancel any stack position animations that were started by calling 364 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end 365 * listeners. 366 */ 367 public void cancelStackPositionAnimations() { 368 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); 369 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); 370 371 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); 372 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); 373 } 374 375 /** Save the current IME height so that we know where the stack bounds should be. */ 376 public void setImeHeight(int imeHeight) { 377 mImeHeight = imeHeight; 378 } 379 380 /** 381 * Animates the stack either away from the newly visible IME, or back to its original position 382 * due to the IME going away. 383 */ 384 public void animateForImeVisibility(boolean imeVisible) { 385 final float maxBubbleY = getAllowableStackPositionRegion().bottom; 386 float destinationY = Float.MIN_VALUE; 387 388 if (imeVisible) { 389 // Stack is lower than it should be and overlaps the now-visible IME. 390 if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) { 391 mPreImeY = mStackPosition.y; 392 destinationY = maxBubbleY; 393 } 394 } else { 395 if (mPreImeY > Float.MIN_VALUE) { 396 destinationY = mPreImeY; 397 mPreImeY = Float.MIN_VALUE; 398 } 399 } 400 401 if (destinationY > Float.MIN_VALUE) { 402 springFirstBubbleWithStackFollowing( 403 DynamicAnimation.TRANSLATION_Y, 404 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null) 405 .setStiffness(SpringForce.STIFFNESS_LOW), 406 /* startVel */ 0f, 407 destinationY); 408 } 409 } 410 411 /** 412 * Returns the region within which the stack is allowed to rest. This goes slightly off the left 413 * and right sides of the screen, below the status bar/cutout and above the navigation bar. 414 * While the stack is not allowed to rest outside of these bounds, it can temporarily be 415 * animated or dragged beyond them. 416 */ 417 public RectF getAllowableStackPositionRegion() { 418 final WindowInsets insets = mLayout.getRootWindowInsets(); 419 final RectF allowableRegion = new RectF(); 420 if (insets != null) { 421 allowableRegion.left = 422 -mBubbleOffscreen 423 + Math.max( 424 insets.getSystemWindowInsetLeft(), 425 insets.getDisplayCutout() != null 426 ? insets.getDisplayCutout().getSafeInsetLeft() 427 : 0); 428 allowableRegion.right = 429 mLayout.getWidth() 430 - mIndividualBubbleSize 431 + mBubbleOffscreen 432 - Math.max( 433 insets.getSystemWindowInsetRight(), 434 insets.getDisplayCutout() != null 435 ? insets.getDisplayCutout().getSafeInsetRight() 436 : 0); 437 438 allowableRegion.top = 439 mBubblePadding 440 + Math.max( 441 mStatusBarHeight, 442 insets.getDisplayCutout() != null 443 ? insets.getDisplayCutout().getSafeInsetTop() 444 : 0); 445 allowableRegion.bottom = 446 mLayout.getHeight() 447 - mIndividualBubbleSize 448 - mBubblePadding 449 - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f) 450 - Math.max( 451 insets.getSystemWindowInsetBottom(), 452 insets.getDisplayCutout() != null 453 ? insets.getDisplayCutout().getSafeInsetBottom() 454 : 0); 455 } 456 457 return allowableRegion; 458 } 459 460 /** Moves the stack in response to a touch event. */ 461 public void moveStackFromTouch(float x, float y) { 462 463 // If we're springing to the touch point to 'catch up' after dragging out of the dismiss 464 // target, then update the stack position animations instead of moving the bubble directly. 465 if (mFirstBubbleSpringingToTouch) { 466 final SpringAnimation springToTouchX = 467 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X); 468 final SpringAnimation springToTouchY = 469 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y); 470 471 // If either animation is still running, we haven't caught up. Update the animations. 472 if (springToTouchX.isRunning() || springToTouchY.isRunning()) { 473 springToTouchX.animateToFinalPosition(x); 474 springToTouchY.animateToFinalPosition(y); 475 } else { 476 // If the animations have finished, the stack is now at the touch point. We can 477 // resume moving the bubble directly. 478 mFirstBubbleSpringingToTouch = false; 479 } 480 } 481 482 if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) { 483 moveFirstBubbleWithStackFollowing(x, y); 484 } 485 } 486 487 /** 488 * Demagnetizes the stack, springing it towards the given point. This also sets flags so that 489 * subsequent touch events will update the final position of the demagnetization spring instead 490 * of directly moving the bubbles, until demagnetization is complete. 491 */ 492 public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) { 493 mWithinDismissTarget = false; 494 mFirstBubbleSpringingToTouch = true; 495 496 springFirstBubbleWithStackFollowing( 497 DynamicAnimation.TRANSLATION_X, 498 new SpringForce() 499 .setDampingRatio(DEFAULT_BOUNCINESS) 500 .setStiffness(DEFAULT_STIFFNESS), 501 velX, x); 502 503 springFirstBubbleWithStackFollowing( 504 DynamicAnimation.TRANSLATION_Y, 505 new SpringForce() 506 .setDampingRatio(DEFAULT_BOUNCINESS) 507 .setStiffness(DEFAULT_STIFFNESS), 508 velY, y); 509 } 510 511 /** 512 * Spring the stack towards the dismiss target, respecting existing velocity. This also sets 513 * flags so that subsequent touch events will not move the stack until it's demagnetized. 514 */ 515 public void magnetToDismiss(float velX, float velY, float destY, Runnable after) { 516 mWithinDismissTarget = true; 517 mFirstBubbleSpringingToTouch = false; 518 519 animationForChildAtIndex(0) 520 .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f) 521 .translationY(destY, after) 522 .withPositionStartVelocities(velX, velY) 523 .withStiffness(SpringForce.STIFFNESS_MEDIUM) 524 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 525 .start(); 526 } 527 528 /** 529 * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out. 530 */ 531 public void implodeStack(Runnable after) { 532 // Pop and fade the bubbles sequentially. 533 animationForChildAtIndex(0) 534 .scaleX(0.5f) 535 .scaleY(0.5f) 536 .alpha(0f) 537 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) 538 .withStiffness(SpringForce.STIFFNESS_HIGH) 539 .start(() -> { 540 // Run the callback and reset flags. The child translation animations might 541 // still be running, but that's fine. Once the alpha is at 0f they're no longer 542 // visible anyway. 543 after.run(); 544 mWithinDismissTarget = false; 545 }); 546 } 547 548 /** 549 * Springs the first bubble to the given final position, with the rest of the stack 'following'. 550 */ 551 protected void springFirstBubbleWithStackFollowing( 552 DynamicAnimation.ViewProperty property, SpringForce spring, 553 float vel, float finalPosition) { 554 555 if (mLayout.getChildCount() == 0) { 556 return; 557 } 558 559 Log.d(TAG, String.format("Springing %s to final position %f.", 560 PhysicsAnimationLayout.getReadablePropertyName(property), 561 finalPosition)); 562 563 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 564 SpringAnimation springAnimation = 565 new SpringAnimation(this, firstBubbleProperty) 566 .setSpring(spring) 567 .setStartVelocity(vel); 568 569 cancelStackPositionAnimation(property); 570 mStackPositionAnimations.put(property, springAnimation); 571 springAnimation.animateToFinalPosition(finalPosition); 572 } 573 574 @Override 575 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 576 return Sets.newHashSet( 577 DynamicAnimation.TRANSLATION_X, // For positioning. 578 DynamicAnimation.TRANSLATION_Y, 579 DynamicAnimation.ALPHA, // For fading in new bubbles. 580 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles. 581 DynamicAnimation.SCALE_Y); 582 } 583 584 @Override 585 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 586 if (property.equals(DynamicAnimation.TRANSLATION_X) 587 || property.equals(DynamicAnimation.TRANSLATION_Y)) { 588 return index + 1; 589 } else if (mWithinDismissTarget) { 590 return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used). 591 } else { 592 return NONE; 593 } 594 } 595 596 597 @Override 598 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { 599 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 600 // If we're in the dismiss target, have the bubbles pile on top of each other with no 601 // offset. 602 if (mWithinDismissTarget) { 603 return 0f; 604 } else { 605 // Offset to the left if we're on the left, or the right otherwise. 606 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x) 607 ? -mStackOffset : mStackOffset; 608 } 609 } else { 610 return 0f; 611 } 612 } 613 614 @Override 615 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 616 return new SpringForce() 617 .setDampingRatio(DEFAULT_BOUNCINESS) 618 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS); 619 } 620 621 @Override 622 void onChildAdded(View child, int index) { 623 if (mLayout.getChildCount() == 1) { 624 // If this is the first child added, position the stack in its starting position. 625 moveStackToStartPosition(); 626 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { 627 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble 628 // to the back of the stack, it'll be largely invisible so don't bother animating it in. 629 animateInBubble(child); 630 } 631 } 632 633 @Override 634 void onChildRemoved(View child, int index, Runnable finishRemoval) { 635 // Animate the removing view in the opposite direction of the stack. 636 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); 637 animationForChild(child) 638 .alpha(0f, finishRemoval /* after */) 639 .scaleX(ANIMATE_IN_STARTING_SCALE) 640 .scaleY(ANIMATE_IN_STARTING_SCALE) 641 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR)) 642 .start(); 643 644 if (mLayout.getChildCount() > 0) { 645 animationForChildAtIndex(0).translationX(mStackPosition.x).start(); 646 } else { 647 // Set the start position back to the default since we're out of bubbles. New bubbles 648 // will then animate in from the start position. 649 mStackPosition = getDefaultStartPosition(); 650 } 651 } 652 653 @Override 654 void onChildReordered(View child, int oldIndex, int newIndex) {} 655 656 @Override 657 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 658 Resources res = layout.getResources(); 659 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 660 mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 661 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); 662 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); 663 mStackStartingVerticalOffset = 664 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); 665 mStatusBarHeight = 666 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 667 } 668 669 /** Moves the stack, without any animation, to the starting position. */ 670 private void moveStackToStartPosition() { 671 // Post to ensure that the layout's width and height have been calculated. 672 mLayout.setVisibility(View.INVISIBLE); 673 mLayout.post(() -> { 674 setStackPosition(mRestingStackPosition == null 675 ? getDefaultStartPosition() 676 : mRestingStackPosition); 677 mStackMovedToStartPosition = true; 678 mLayout.setVisibility(View.VISIBLE); 679 680 // Animate in the top bubble now that we're visible. 681 if (mLayout.getChildCount() > 0) { 682 animateInBubble(mLayout.getChildAt(0)); 683 } 684 }); 685 } 686 687 /** 688 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent 689 * bubbles to animate 'following' to the new location. 690 */ 691 private void moveFirstBubbleWithStackFollowing( 692 DynamicAnimation.ViewProperty property, float value) { 693 694 // Update the canonical stack position. 695 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 696 mStackPosition.x = value; 697 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 698 mStackPosition.y = value; 699 } 700 701 if (mLayout.getChildCount() > 0) { 702 property.setValue(mLayout.getChildAt(0), value); 703 if (mLayout.getChildCount() > 1) { 704 animationForChildAtIndex(1) 705 .property(property, value + getOffsetForChainedPropertyAnimation(property)) 706 .start(); 707 } 708 } 709 } 710 711 /** Moves the stack to a position instantly, with no animation. */ 712 private void setStackPosition(PointF pos) { 713 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); 714 mStackPosition.set(pos.x, pos.y); 715 716 // If we're not the active controller, we don't want to physically move the bubble views. 717 if (isActiveController()) { 718 mLayout.cancelAllAnimations(); 719 cancelStackPositionAnimations(); 720 721 // Since we're not using the chained animations, apply the offsets manually. 722 final float xOffset = getOffsetForChainedPropertyAnimation( 723 DynamicAnimation.TRANSLATION_X); 724 final float yOffset = getOffsetForChainedPropertyAnimation( 725 DynamicAnimation.TRANSLATION_Y); 726 for (int i = 0; i < mLayout.getChildCount(); i++) { 727 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); 728 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); 729 } 730 } 731 } 732 733 /** Returns the default stack position, which is on the top right. */ 734 private PointF getDefaultStartPosition() { 735 return new PointF( 736 getAllowableStackPositionRegion().right, 737 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset); 738 } 739 740 private boolean isStackPositionSet() { 741 return mStackMovedToStartPosition; 742 } 743 744 /** Animates in the given bubble. */ 745 private void animateInBubble(View child) { 746 if (!isActiveController()) { 747 return; 748 } 749 750 child.setTranslationY(mStackPosition.y); 751 752 float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); 753 animationForChild(child) 754 .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) 755 .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) 756 .alpha(0f /* from */, 1f /* to */) 757 .translationX( 758 mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */, 759 mStackPosition.x /* to */) 760 .start(); 761 } 762 763 /** 764 * Cancels any outstanding first bubble property animations that are running. This does not 765 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only 766 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and 767 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. 768 */ 769 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) { 770 if (mStackPositionAnimations.containsKey(property)) { 771 mStackPositionAnimations.get(property).cancel(); 772 } 773 } 774 775 /** 776 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's 777 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this 778 * property directly to move the first bubble and cause the stack to 'follow' to the new 779 * location. 780 * 781 * This could also be achieved by simply animating the first bubble view and adding an update 782 * listener to dispatch movement to the rest of the stack. However, this would require 783 * duplication of logic in that update handler - it's simpler to keep all logic contained in the 784 * {@link #moveFirstBubbleWithStackFollowing} method. 785 */ 786 private class StackPositionProperty 787 extends FloatPropertyCompat<StackAnimationController> { 788 private final DynamicAnimation.ViewProperty mProperty; 789 790 private StackPositionProperty(DynamicAnimation.ViewProperty property) { 791 super(property.toString()); 792 mProperty = property; 793 } 794 795 @Override 796 public float getValue(StackAnimationController controller) { 797 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0; 798 } 799 800 @Override 801 public void setValue(StackAnimationController controller, float value) { 802 moveFirstBubbleWithStackFollowing(mProperty, value); 803 } 804 } 805 } 806 807