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