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