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 android.view.View.LAYOUT_DIRECTION_RTL; 20 21 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; 22 import static com.android.wm.shell.bubbles.animation.FlingToDismissUtils.getFlingToDismissTargetWidth; 23 24 import android.content.res.Resources; 25 import android.graphics.Path; 26 import android.graphics.PointF; 27 import android.view.View; 28 import android.view.animation.Interpolator; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.dynamicanimation.animation.DynamicAnimation; 33 import androidx.dynamicanimation.animation.SpringForce; 34 35 import com.android.wm.shell.R; 36 import com.android.wm.shell.animation.Interpolators; 37 import com.android.wm.shell.bubbles.BadgedImageView; 38 import com.android.wm.shell.bubbles.BubbleOverflow; 39 import com.android.wm.shell.bubbles.BubblePositioner; 40 import com.android.wm.shell.bubbles.BubbleStackView; 41 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 42 import com.android.wm.shell.shared.animation.PhysicsAnimator; 43 44 import com.google.android.collect.Sets; 45 46 import java.io.PrintWriter; 47 import java.util.Set; 48 49 /** 50 * Animation controller for bubbles when they're in their expanded state, or animating to/from the 51 * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be 52 * dismissed. 53 */ 54 public class ExpandedAnimationController 55 extends PhysicsAnimationLayout.PhysicsAnimationController { 56 57 /** 58 * How much to translate the bubbles when they're animating in/out. This value is multiplied by 59 * the bubble size. 60 */ 61 private static final int ANIMATE_TRANSLATION_FACTOR = 4; 62 63 /** Duration of the expand/collapse target path animation. */ 64 public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175; 65 66 /** Damping ratio for expand/collapse spring. */ 67 private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f; 68 69 /** 70 * Damping ratio for the overflow bubble spring; this is less bouncy so it doesn't bounce behind 71 * the top bubble when it goes to disappear. 72 */ 73 private static final float DAMPING_RATIO_OVERFLOW_BOUNCY = 0.90f; 74 75 /** Stiffness for the expand/collapse path-following animation. */ 76 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 400; 77 78 /** Stiffness for the expand/collapse animation when home gesture handling is off */ 79 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE = 1000; 80 81 /** 82 * Velocity required to dismiss an individual bubble without dragging it into the dismiss 83 * target. 84 */ 85 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f; 86 87 private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = 88 new PhysicsAnimator.SpringConfig( 89 EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); 90 91 /** Horizontal offset between bubbles, which we need to know to re-stack them. */ 92 private float mStackOffsetPx; 93 /** Size of each bubble. */ 94 private float mBubbleSizePx; 95 /** Whether the expand / collapse animation is running. */ 96 private boolean mAnimatingExpand = false; 97 98 /** 99 * Whether we are animating other Bubbles UI elements out in preparation for a call to 100 * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or 101 * reorders. 102 */ 103 private boolean mPreparingToCollapse = false; 104 105 private boolean mAnimatingCollapse = false; 106 @Nullable 107 private Runnable mAfterExpand; 108 private Runnable mAfterCollapse; 109 private PointF mCollapsePoint; 110 private boolean mFadeBubblesDuringCollapse = false; 111 112 /** 113 * Whether the dragged out bubble is springing towards the touch point, rather than using the 114 * default behavior of moving directly to the touch point. 115 * 116 * This happens when the user's finger exits the dismiss area while the bubble is magnetized to 117 * the center. Since the touch point differs from the bubble location, we need to animate the 118 * bubble back to the touch point to avoid a jarring instant location change from the center of 119 * the target to the touch point just outside the target bounds. 120 */ 121 private boolean mSpringingBubbleToTouch = false; 122 123 /** 124 * Whether to spring the bubble to the next touch event coordinates. This is used to animate the 125 * bubble out of the magnetic dismiss target to the touch location. 126 * 127 * Once it 'catches up' and the animation ends, we'll revert to moving it directly. 128 */ 129 private boolean mSpringToTouchOnNextMotionEvent = false; 130 131 /** The bubble currently being dragged out of the row (to potentially be dismissed). */ 132 private MagnetizedObject<View> mMagnetizedBubbleDraggingOut; 133 134 /** 135 * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the 136 * end of this animation means we have no bubbles left, and notify the BubbleController. 137 */ 138 private Runnable mOnBubbleAnimatedOutAction; 139 140 private BubblePositioner mPositioner; 141 142 private BubbleStackView mBubbleStackView; 143 144 /** 145 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause 146 * the rest of the bubbles to animate to fill the gap. 147 */ 148 private boolean mBubbleDraggedOutEnough = false; 149 150 /** End action to run when the lead bubble's expansion animation completes. */ 151 @Nullable 152 private Runnable mLeadBubbleEndAction; 153 ExpandedAnimationController(BubblePositioner positioner, Runnable onBubbleAnimatedOutAction, BubbleStackView stackView)154 public ExpandedAnimationController(BubblePositioner positioner, 155 Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) { 156 mPositioner = positioner; 157 updateResources(); 158 mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; 159 mCollapsePoint = mPositioner.getDefaultStartPosition(); 160 mBubbleStackView = stackView; 161 } 162 163 /** 164 * Overrides the collapse location without actually collapsing the stack. 165 * @param point the new collapse location. 166 */ setCollapsePoint(PointF point)167 public void setCollapsePoint(PointF point) { 168 mCollapsePoint = point; 169 } 170 171 /** 172 * Animates expanding the bubbles into a row along the top of the screen, optionally running an 173 * end action when the entire animation completes, and an end action when the lead bubble's 174 * animation ends. 175 */ expandFromStack( @ullable Runnable after, @Nullable Runnable leadBubbleEndAction)176 public void expandFromStack( 177 @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) { 178 mPreparingToCollapse = false; 179 mAnimatingCollapse = false; 180 mAnimatingExpand = true; 181 mAfterExpand = after; 182 mLeadBubbleEndAction = leadBubbleEndAction; 183 184 startOrUpdatePathAnimation(true /* expanding */); 185 } 186 187 /** 188 * Animates expanding the bubbles into a row along the top of the screen. 189 */ expandFromStack(@ullable Runnable after)190 public void expandFromStack(@Nullable Runnable after) { 191 expandFromStack(after, null /* leadBubbleEndAction */); 192 } 193 194 /** 195 * Sets that we're animating the stack collapsed, but haven't yet called 196 * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are 197 * added or re-ordered, since the upcoming collapse animation will handle positioning those 198 * bubbles in the collapsed stack. 199 */ notifyPreparingToCollapse()200 public void notifyPreparingToCollapse() { 201 mPreparingToCollapse = true; 202 } 203 204 /** Animate collapsing the bubbles back to their stacked position. */ collapseBackToStack(PointF collapsePoint, boolean fadeBubblesDuringCollapse, Runnable after)205 public void collapseBackToStack(PointF collapsePoint, boolean fadeBubblesDuringCollapse, 206 Runnable after) { 207 mAnimatingExpand = false; 208 mPreparingToCollapse = false; 209 mAnimatingCollapse = true; 210 mAfterCollapse = after; 211 mCollapsePoint = collapsePoint; 212 mFadeBubblesDuringCollapse = fadeBubblesDuringCollapse; 213 214 startOrUpdatePathAnimation(false /* expanding */); 215 } 216 217 /** 218 * Update effective screen width based on current orientation. 219 */ updateResources()220 public void updateResources() { 221 if (mLayout == null) { 222 return; 223 } 224 Resources res = mLayout.getContext().getResources(); 225 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 226 mBubbleSizePx = mPositioner.getBubbleSize(); 227 } 228 229 /** 230 * Animates the bubbles along a curved path, either to expand them along the top or collapse 231 * them back into a stack. 232 */ startOrUpdatePathAnimation(boolean expanding)233 private void startOrUpdatePathAnimation(boolean expanding) { 234 Runnable after; 235 236 if (expanding) { 237 after = () -> { 238 mAnimatingExpand = false; 239 240 if (mAfterExpand != null) { 241 mAfterExpand.run(); 242 } 243 244 mAfterExpand = null; 245 246 // Update bubble positions in case any bubbles were added or removed during the 247 // expansion animation. 248 updateBubblePositions(); 249 }; 250 } else { 251 after = () -> { 252 mAnimatingCollapse = false; 253 254 if (mAfterCollapse != null) { 255 mAfterCollapse.run(); 256 } 257 258 mAfterCollapse = null; 259 mFadeBubblesDuringCollapse = false; 260 }; 261 } 262 263 boolean showBubblesVertically = mPositioner.showBubblesVertically(); 264 final boolean isRtl = 265 mLayout.getContext().getResources().getConfiguration().getLayoutDirection() 266 == LAYOUT_DIRECTION_RTL; 267 268 // Animate each bubble individually, since each path will end in a different spot. 269 animationsForChildrenFromIndex(0, mFadeBubblesDuringCollapse, (index, animation) -> { 270 final View bubble = mLayout.getChildAt(index); 271 272 // Start a path at the bubble's current position. 273 final Path path = new Path(); 274 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); 275 276 final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); 277 if (expanding) { 278 // If we're expanding, first draw a line from the bubble's current position to where 279 // it'll end up 280 path.lineTo(bubble.getTranslationX(), p.y); 281 // Then, draw a line across the screen to the bubble's resting position. 282 path.lineTo(p.x, p.y); 283 } else { 284 final float stackedX = mCollapsePoint.x; 285 286 // If we're collapsing, draw a line from the bubble's current position to the side 287 // of the screen where the bubble will be stacked. 288 path.lineTo(stackedX, p.y); 289 290 // The overflow should animate to the collapse point, so 0 offset. 291 final boolean isOverflow = bubble instanceof BadgedImageView 292 && BubbleOverflow.KEY.equals(((BadgedImageView) bubble).getKey()); 293 final float offsetY = isOverflow 294 ? 0 295 : Math.min(index, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffsetPx; 296 // Then, draw a line down to the stack position. 297 path.lineTo(stackedX, mCollapsePoint.y + offsetY); 298 } 299 300 // The lead bubble should be the bubble with the longest distance to travel when we're 301 // expanding, and the bubble with the shortest distance to travel when we're collapsing. 302 // During expansion from the left side, the last bubble has to travel to the far right 303 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the 304 // right side, the first bubble is traveling to the top left, so it leads. During 305 // collapse to the left, the first bubble has the shortest travel time back to the stack 306 // position, so it leads (and vice versa). 307 final boolean firstBubbleLeads; 308 if (showBubblesVertically || !isRtl) { 309 firstBubbleLeads = 310 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) 311 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); 312 } else { 313 // For RTL languages, when showing bubbles horizontally, it is reversed. The bubbles 314 // are positioned right to left. This means that when expanding from left, the top 315 // bubble will lead as it will be positioned on the right. And when expanding from 316 // right, the top bubble will have the least travel distance. 317 firstBubbleLeads = 318 (expanding && mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) 319 || (!expanding && !mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); 320 } 321 final int startDelay = firstBubbleLeads 322 ? (index * 10) 323 : ((mLayout.getChildCount() - index) * 10); 324 325 final boolean isLeadBubble = 326 (firstBubbleLeads && index == 0) 327 || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); 328 329 Interpolator interpolator = expanding 330 ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE; 331 332 animation 333 .followAnimatedTargetAlongPath( 334 path, 335 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, 336 interpolator /* targetAnimInterpolator */, 337 isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, 338 () -> mLeadBubbleEndAction = null /* endAction */) 339 .withStartDelay(startDelay) 340 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); 341 }).startAll(after); 342 } 343 344 /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */ onUnstuckFromTarget()345 public void onUnstuckFromTarget() { 346 mSpringToTouchOnNextMotionEvent = true; 347 } 348 349 /** 350 * Prepares the given bubble view to be dragged out, using the provided magnetic target and 351 * listener. 352 */ prepareForBubbleDrag( View bubble, MagnetizedObject.MagneticTarget target, MagnetizedObject.MagnetListener listener)353 public void prepareForBubbleDrag( 354 View bubble, 355 MagnetizedObject.MagneticTarget target, 356 MagnetizedObject.MagnetListener listener) { 357 mLayout.cancelAnimationsOnView(bubble); 358 359 mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>( 360 mLayout.getContext(), bubble, 361 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { 362 @Override 363 public float getWidth(@NonNull View underlyingObject) { 364 return mBubbleSizePx; 365 } 366 367 @Override 368 public float getHeight(@NonNull View underlyingObject) { 369 return mBubbleSizePx; 370 } 371 372 @Override 373 public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) { 374 loc[0] = (int) bubble.getTranslationX(); 375 loc[1] = (int) bubble.getTranslationY(); 376 } 377 }; 378 mMagnetizedBubbleDraggingOut.addTarget(target); 379 mMagnetizedBubbleDraggingOut.setMagnetListener(listener); 380 mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); 381 mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); 382 int screenWidthPx = mLayout.getContext().getResources().getDisplayMetrics().widthPixels; 383 mMagnetizedBubbleDraggingOut.setFlingToTargetWidthPercent( 384 getFlingToDismissTargetWidth(screenWidthPx)); 385 } 386 springBubbleTo(View bubble, float x, float y)387 private void springBubbleTo(View bubble, float x, float y) { 388 animationForChild(bubble) 389 .translationX(x) 390 .translationY(y) 391 .withStiffness(SpringForce.STIFFNESS_HIGH) 392 .start(); 393 } 394 395 /** 396 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to 397 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the 398 * bubble is dragged back into the row. 399 */ dragBubbleOut(View bubbleView, float x, float y)400 public void dragBubbleOut(View bubbleView, float x, float y) { 401 if (mMagnetizedBubbleDraggingOut == null) { 402 return; 403 } 404 if (mSpringToTouchOnNextMotionEvent) { 405 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); 406 mSpringToTouchOnNextMotionEvent = false; 407 mSpringingBubbleToTouch = true; 408 } else if (mSpringingBubbleToTouch) { 409 if (mLayout.arePropertiesAnimatingOnView( 410 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { 411 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); 412 } else { 413 mSpringingBubbleToTouch = false; 414 } 415 } 416 417 if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) { 418 bubbleView.setTranslationX(x); 419 bubbleView.setTranslationY(y); 420 } 421 422 final int expandedY = mPositioner.getExpandedViewYTopAligned(); 423 final boolean draggedOutEnough = 424 y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx; 425 if (draggedOutEnough != mBubbleDraggedOutEnough) { 426 updateBubblePositions(); 427 mBubbleDraggedOutEnough = draggedOutEnough; 428 } 429 } 430 431 /** Plays a dismiss animation on the dragged out bubble. */ 432 public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) { 433 if (bubble == null) { 434 return; 435 } 436 animationForChild(bubble) 437 .withStiffness(SpringForce.STIFFNESS_HIGH) 438 .scaleX(0f) 439 .scaleY(0f) 440 .translationY(bubble.getTranslationY() + translationYBy) 441 .alpha(0f, after) 442 .start(); 443 444 updateBubblePositions(); 445 } 446 447 @Nullable 448 public View getDraggedOutBubble() { 449 return mMagnetizedBubbleDraggingOut == null 450 ? null 451 : mMagnetizedBubbleDraggingOut.getUnderlyingObject(); 452 } 453 454 /** Returns the MagnetizedObject instance for the dragging-out bubble. */ 455 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() { 456 return mMagnetizedBubbleDraggingOut; 457 } 458 459 /** 460 * Snaps a bubble back to its position within the bubble row, and animates the rest of the 461 * bubbles to accommodate it if it was previously dragged out past the threshold. 462 * Only happens while the stack is expanded. 463 */ 464 public void snapBubbleBack(View bubbleView, float velX, float velY) { 465 if (mLayout == null) { 466 return; 467 } 468 final int index = mLayout.indexOfChild(bubbleView); 469 final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); 470 // overflow is not draggable so it's never the overflow 471 final float zTranslation = mPositioner.getZTranslation(index, 472 false /* isOverflow */, 473 true /* isExpanded */); 474 animationForChildAtIndex(index) 475 .position(p.x, p.y, zTranslation) 476 .withPositionStartVelocities(velX, velY) 477 .start(); 478 479 mMagnetizedBubbleDraggingOut = null; 480 481 updateBubblePositions(); 482 } 483 484 /** Resets bubble drag out gesture flags. */ 485 public void onGestureFinished() { 486 mBubbleDraggedOutEnough = false; 487 mMagnetizedBubbleDraggingOut = null; 488 updateBubblePositions(); 489 } 490 491 /** Description of current animation controller state. */ 492 public void dump(PrintWriter pw) { 493 pw.println("ExpandedAnimationController state:"); 494 pw.print(" isActive: "); pw.println(isActiveController()); 495 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand); 496 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse); 497 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch); 498 } 499 500 @Override 501 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 502 updateResources(); 503 504 // Ensure that all child views are at 1x scale, and visible, in case they were animating 505 // in. 506 mLayout.setVisibility(View.VISIBLE); 507 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) -> 508 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll(); 509 } 510 511 @Override getAnimatedProperties()512 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 513 return Sets.newHashSet( 514 DynamicAnimation.TRANSLATION_X, 515 DynamicAnimation.TRANSLATION_Y, 516 DynamicAnimation.TRANSLATION_Z, 517 DynamicAnimation.SCALE_X, 518 DynamicAnimation.SCALE_Y, 519 DynamicAnimation.ALPHA); 520 } 521 522 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)523 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 524 return NONE; 525 } 526 527 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)528 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index) { 529 return 0; 530 } 531 532 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)533 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 534 boolean isOverflow = (view instanceof BadgedImageView) 535 && BubbleOverflow.KEY.equals(((BadgedImageView) view).getKey()); 536 return new SpringForce() 537 .setDampingRatio(isOverflow 538 ? DAMPING_RATIO_OVERFLOW_BOUNCY 539 : DAMPING_RATIO_MEDIUM_LOW_BOUNCY) 540 .setStiffness(SpringForce.STIFFNESS_LOW); 541 } 542 543 @Override onChildAdded(View child, int index)544 void onChildAdded(View child, int index) { 545 // If a bubble is added while the expand/collapse animations are playing, update the 546 // animation to include the new bubble. 547 if (mAnimatingExpand) { 548 startOrUpdatePathAnimation(true /* expanding */); 549 } else if (mAnimatingCollapse) { 550 startOrUpdatePathAnimation(false /* expanding */); 551 } else { 552 boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); 553 final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); 554 if (mPositioner.showBubblesVertically()) { 555 child.setTranslationY(p.y); 556 } else { 557 child.setTranslationX(p.x); 558 } 559 560 if (mPreparingToCollapse) { 561 // Don't animate if we're collapsing, as that animation will handle placing the 562 // new bubble in the stacked position. 563 return; 564 } 565 566 if (mPositioner.showBubblesVertically()) { 567 float fromX = onLeft 568 ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR 569 : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; 570 animationForChild(child) 571 .translationX(fromX, p.x) 572 .start(); 573 } else { 574 float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; 575 animationForChild(child) 576 .translationY(fromY, p.y) 577 .start(); 578 } 579 updateBubblePositions(); 580 } 581 } 582 583 @Override onChildRemoved(View child, int index, Runnable finishRemoval)584 void onChildRemoved(View child, int index, Runnable finishRemoval) { 585 // If we're removing the dragged-out bubble, that means it got dismissed. 586 if (child.equals(getDraggedOutBubble())) { 587 mMagnetizedBubbleDraggingOut = null; 588 finishRemoval.run(); 589 mOnBubbleAnimatedOutAction.run(); 590 } else { 591 PhysicsAnimator.getInstance(child) 592 .spring(DynamicAnimation.ALPHA, 0f) 593 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) 594 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) 595 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) 596 .start(); 597 } 598 599 // Animate all the other bubbles to their new positions sans this bubble. 600 updateBubblePositions(); 601 } 602 603 @Override onChildReordered(View child, int oldIndex, int newIndex)604 void onChildReordered(View child, int oldIndex, int newIndex) { 605 if (mPreparingToCollapse) { 606 // If a re-order is received while we're preparing to collapse, ignore it. Once started, 607 // the collapse animation will animate all of the bubbles to their correct (stacked) 608 // position. 609 return; 610 } 611 612 if (mAnimatingCollapse) { 613 // If a re-order is received during collapse, update the animation so that the bubbles 614 // end up in the correct (stacked) position. 615 startOrUpdatePathAnimation(false /* expanding */); 616 } else { 617 // Otherwise, animate the bubbles around to reflect their new order. 618 updateBubblePositions(); 619 } 620 } 621 622 /** 623 * Call to update the bubble positions after an orientation change. 624 */ onOrientationChanged()625 public void onOrientationChanged() { 626 if (mLayout == null) return; 627 updateBubblePositions(); 628 } 629 updateBubblePositions()630 private void updateBubblePositions() { 631 if (mAnimatingExpand || mAnimatingCollapse) { 632 return; 633 } 634 for (int i = 0; i < mLayout.getChildCount(); i++) { 635 final View bubble = mLayout.getChildAt(i); 636 637 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It 638 // will be snapped to the correct X value after the drag (if it's not dismissed). 639 if (bubble.equals(getDraggedOutBubble())) { 640 return; 641 } 642 643 final PointF p = mPositioner.getExpandedBubbleXY(i, mBubbleStackView.getState()); 644 animationForChild(bubble) 645 .translationX(p.x) 646 .translationY(p.y) 647 .start(); 648 } 649 } 650 651 /** Returns true if we're in the middle of a collapse or expand animation. */ isAnimating()652 boolean isAnimating() { 653 return mAnimatingCollapse || mAnimatingExpand; 654 } 655 } 656