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