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