1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.bubbles.animation; 18 19 import android.content.res.Configuration; 20 import android.content.res.Resources; 21 import android.graphics.Path; 22 import android.graphics.Point; 23 import android.graphics.PointF; 24 import android.view.DisplayCutout; 25 import android.view.View; 26 import android.view.WindowInsets; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.dynamicanimation.animation.DynamicAnimation; 31 import androidx.dynamicanimation.animation.SpringForce; 32 33 import com.android.systemui.Interpolators; 34 import com.android.systemui.R; 35 import com.android.systemui.util.animation.PhysicsAnimator; 36 import com.android.systemui.util.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 /** Stiffness for the expand/collapse path-following animation. */ 62 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; 63 64 /** What percentage of the screen to use when centering the bubbles in landscape. */ 65 private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f; 66 67 /** 68 * Velocity required to dismiss an individual bubble without dragging it into the dismiss 69 * target. 70 */ 71 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f; 72 73 private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = 74 new PhysicsAnimator.SpringConfig( 75 EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); 76 77 /** Horizontal offset between bubbles, which we need to know to re-stack them. */ 78 private float mStackOffsetPx; 79 /** Space between status bar and bubbles in the expanded state. */ 80 private float mBubblePaddingTop; 81 /** Size of each bubble. */ 82 private float mBubbleSizePx; 83 /** Space between bubbles in row above expanded view. */ 84 private float mSpaceBetweenBubbles; 85 /** Height of the status bar. */ 86 private float mStatusBarHeight; 87 /** Size of display. */ 88 private Point mDisplaySize; 89 /** Max number of bubbles shown in row above expanded view. */ 90 private int mBubblesMaxRendered; 91 /** What the current screen orientation is. */ 92 private int mScreenOrientation; 93 94 private boolean mAnimatingExpand = false; 95 96 /** 97 * Whether we are animating other Bubbles UI elements out in preparation for a call to 98 * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or 99 * reorders. 100 */ 101 private boolean mPreparingToCollapse = false; 102 103 private boolean mAnimatingCollapse = false; 104 private @Nullable 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 ExpandedAnimationController(Point displaySize, int expandedViewPadding, int orientation, Runnable onBubbleAnimatedOutAction)138 public ExpandedAnimationController(Point displaySize, int expandedViewPadding, 139 int orientation, Runnable onBubbleAnimatedOutAction) { 140 updateResources(orientation, displaySize); 141 mExpandedViewPadding = expandedViewPadding; 142 mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; 143 } 144 145 /** 146 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause 147 * the rest of the bubbles to animate to fill the gap. 148 */ 149 private boolean mBubbleDraggedOutEnough = false; 150 151 /** End action to run when the lead bubble's expansion animation completes. */ 152 @Nullable 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 * @param orientation Landscape or portrait. 201 * @param displaySize Updated display size. 202 */ updateResources(int orientation, Point displaySize)203 public void updateResources(int orientation, Point displaySize) { 204 mScreenOrientation = orientation; 205 mDisplaySize = displaySize; 206 if (mLayout == null) { 207 return; 208 } 209 Resources res = mLayout.getContext().getResources(); 210 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 211 mStatusBarHeight = res.getDimensionPixelSize( 212 com.android.internal.R.dimen.status_bar_height); 213 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 214 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 215 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 216 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered); 217 218 // Includes overflow button. 219 float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2) 220 - (mBubblesMaxRendered + 1) * mBubbleSizePx; 221 mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered; 222 } 223 224 /** 225 * Animates the bubbles along a curved path, either to expand them along the top or collapse 226 * them back into a stack. 227 */ startOrUpdatePathAnimation(boolean expanding)228 private void startOrUpdatePathAnimation(boolean expanding) { 229 Runnable after; 230 231 if (expanding) { 232 after = () -> { 233 mAnimatingExpand = false; 234 235 if (mAfterExpand != null) { 236 mAfterExpand.run(); 237 } 238 239 mAfterExpand = null; 240 241 // Update bubble positions in case any bubbles were added or removed during the 242 // expansion animation. 243 updateBubblePositions(); 244 }; 245 } else { 246 after = () -> { 247 mAnimatingCollapse = false; 248 249 if (mAfterCollapse != null) { 250 mAfterCollapse.run(); 251 } 252 253 mAfterCollapse = null; 254 }; 255 } 256 257 // Animate each bubble individually, since each path will end in a different spot. 258 animationsForChildrenFromIndex(0, (index, animation) -> { 259 final View bubble = mLayout.getChildAt(index); 260 261 // Start a path at the bubble's current position. 262 final Path path = new Path(); 263 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); 264 265 final float expandedY = getExpandedY(); 266 if (expanding) { 267 // If we're expanding, first draw a line from the bubble's current position to the 268 // top of the screen. 269 path.lineTo(bubble.getTranslationX(), expandedY); 270 271 // Then, draw a line across the screen to the bubble's resting position. 272 path.lineTo(getBubbleLeft(index), expandedY); 273 } else { 274 final float sideMultiplier = 275 mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; 276 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx); 277 278 // If we're collapsing, draw a line from the bubble's current position to the side 279 // of the screen where the bubble will be stacked. 280 path.lineTo(stackedX, expandedY); 281 282 // Then, draw a line down to the stack position. 283 path.lineTo(stackedX, mCollapsePoint.y); 284 } 285 286 // The lead bubble should be the bubble with the longest distance to travel when we're 287 // expanding, and the bubble with the shortest distance to travel when we're collapsing. 288 // During expansion from the left side, the last bubble has to travel to the far right 289 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the 290 // right side, the first bubble is traveling to the top left, so it leads. During 291 // collapse to the left, the first bubble has the shortest travel time back to the stack 292 // position, so it leads (and vice versa). 293 final boolean firstBubbleLeads = 294 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) 295 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); 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 animation 305 .followAnimatedTargetAlongPath( 306 path, 307 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, 308 Interpolators.LINEAR /* targetAnimInterpolator */, 309 isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, 310 () -> mLeadBubbleEndAction = null /* endAction */) 311 .withStartDelay(startDelay) 312 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); 313 }).startAll(after); 314 } 315 316 /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */ onUnstuckFromTarget()317 public void onUnstuckFromTarget() { 318 mSpringToTouchOnNextMotionEvent = true; 319 } 320 321 /** 322 * Prepares the given bubble view to be dragged out, using the provided magnetic target and 323 * listener. 324 */ prepareForBubbleDrag( View bubble, MagnetizedObject.MagneticTarget target, MagnetizedObject.MagnetListener listener)325 public void prepareForBubbleDrag( 326 View bubble, 327 MagnetizedObject.MagneticTarget target, 328 MagnetizedObject.MagnetListener listener) { 329 mLayout.cancelAnimationsOnView(bubble); 330 331 bubble.setTranslationZ(Short.MAX_VALUE); 332 mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>( 333 mLayout.getContext(), bubble, 334 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { 335 @Override 336 public float getWidth(@NonNull View underlyingObject) { 337 return mBubbleSizePx; 338 } 339 340 @Override 341 public float getHeight(@NonNull View underlyingObject) { 342 return mBubbleSizePx; 343 } 344 345 @Override 346 public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) { 347 loc[0] = (int) bubble.getTranslationX(); 348 loc[1] = (int) bubble.getTranslationY(); 349 } 350 }; 351 mMagnetizedBubbleDraggingOut.addTarget(target); 352 mMagnetizedBubbleDraggingOut.setMagnetListener(listener); 353 mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); 354 mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); 355 } 356 springBubbleTo(View bubble, float x, float y)357 private void springBubbleTo(View bubble, float x, float y) { 358 animationForChild(bubble) 359 .translationX(x) 360 .translationY(y) 361 .withStiffness(SpringForce.STIFFNESS_HIGH) 362 .start(); 363 } 364 365 /** 366 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to 367 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the 368 * bubble is dragged back into the row. 369 */ dragBubbleOut(View bubbleView, float x, float y)370 public void dragBubbleOut(View bubbleView, float x, float y) { 371 if (mSpringToTouchOnNextMotionEvent) { 372 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); 373 mSpringToTouchOnNextMotionEvent = false; 374 mSpringingBubbleToTouch = true; 375 } else if (mSpringingBubbleToTouch) { 376 if (mLayout.arePropertiesAnimatingOnView( 377 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { 378 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); 379 } else { 380 mSpringingBubbleToTouch = false; 381 } 382 } 383 384 if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) { 385 bubbleView.setTranslationX(x); 386 bubbleView.setTranslationY(y); 387 } 388 389 final boolean draggedOutEnough = 390 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; 391 if (draggedOutEnough != mBubbleDraggedOutEnough) { 392 updateBubblePositions(); 393 mBubbleDraggedOutEnough = draggedOutEnough; 394 } 395 } 396 397 /** Plays a dismiss animation on the dragged out bubble. */ 398 public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) { 399 if (bubble == null) { 400 return; 401 } 402 animationForChild(bubble) 403 .withStiffness(SpringForce.STIFFNESS_HIGH) 404 .scaleX(0f) 405 .scaleY(0f) 406 .translationY(bubble.getTranslationY() + translationYBy) 407 .alpha(0f, after) 408 .start(); 409 410 updateBubblePositions(); 411 } 412 413 @Nullable public View getDraggedOutBubble() { 414 return mMagnetizedBubbleDraggingOut == null 415 ? null 416 : mMagnetizedBubbleDraggingOut.getUnderlyingObject(); 417 } 418 419 /** Returns the MagnetizedObject instance for the dragging-out bubble. */ 420 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() { 421 return mMagnetizedBubbleDraggingOut; 422 } 423 424 /** 425 * Snaps a bubble back to its position within the bubble row, and animates the rest of the 426 * bubbles to accommodate it if it was previously dragged out past the threshold. 427 */ 428 public void snapBubbleBack(View bubbleView, float velX, float velY) { 429 final int index = mLayout.indexOfChild(bubbleView); 430 431 animationForChildAtIndex(index) 432 .position(getBubbleLeft(index), getExpandedY()) 433 .withPositionStartVelocities(velX, velY) 434 .start(() -> bubbleView.setTranslationZ(0f) /* after */); 435 436 mMagnetizedBubbleDraggingOut = null; 437 438 updateBubblePositions(); 439 } 440 441 /** Resets bubble drag out gesture flags. */ onGestureFinished()442 public void onGestureFinished() { 443 mBubbleDraggedOutEnough = false; 444 mMagnetizedBubbleDraggingOut = null; 445 updateBubblePositions(); 446 } 447 448 /** 449 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. 450 */ updateYPosition(Runnable after)451 public void updateYPosition(Runnable after) { 452 if (mLayout == null) return; 453 animationsForChildrenFromIndex( 454 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); 455 } 456 457 /** The Y value of the row of expanded bubbles. */ getExpandedY()458 public float getExpandedY() { 459 if (mLayout == null || mLayout.getRootWindowInsets() == null) { 460 return 0; 461 } 462 final WindowInsets insets = mLayout.getRootWindowInsets(); 463 return mBubblePaddingTop + Math.max( 464 mStatusBarHeight, 465 insets.getDisplayCutout() != null 466 ? insets.getDisplayCutout().getSafeInsetTop() 467 : 0); 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(mScreenOrientation, mDisplaySize); 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)506 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { 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(SpringForce.DAMPING_RATIO_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 { 526 child.setTranslationX(getBubbleLeft(index)); 527 528 // If we're preparing to collapse, don't start animations since the collapse animation 529 // will take over and animate the new bubble into the correct (stacked) position. 530 if (!mPreparingToCollapse) { 531 animationForChild(child) 532 .translationY( 533 getExpandedY() 534 - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ 535 getExpandedY() /* to */) 536 .start(); 537 updateBubblePositions(); 538 } 539 } 540 } 541 542 @Override onChildRemoved(View child, int index, Runnable finishRemoval)543 void onChildRemoved(View child, int index, Runnable finishRemoval) { 544 // If we're removing the dragged-out bubble, that means it got dismissed. 545 if (child.equals(getDraggedOutBubble())) { 546 mMagnetizedBubbleDraggingOut = null; 547 finishRemoval.run(); 548 mOnBubbleAnimatedOutAction.run(); 549 } else { 550 PhysicsAnimator.getInstance(child) 551 .spring(DynamicAnimation.ALPHA, 0f) 552 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) 553 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) 554 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) 555 .start(); 556 } 557 558 // Animate all the other bubbles to their new positions sans this bubble. 559 updateBubblePositions(); 560 } 561 562 @Override onChildReordered(View child, int oldIndex, int newIndex)563 void onChildReordered(View child, int oldIndex, int newIndex) { 564 if (mPreparingToCollapse) { 565 // If a re-order is received while we're preparing to collapse, ignore it. Once started, 566 // the collapse animation will animate all of the bubbles to their correct (stacked) 567 // position. 568 return; 569 } 570 571 if (mAnimatingCollapse) { 572 // If a re-order is received during collapse, update the animation so that the bubbles 573 // end up in the correct (stacked) position. 574 startOrUpdatePathAnimation(false /* expanding */); 575 } else { 576 // Otherwise, animate the bubbles around to reflect their new order. 577 updateBubblePositions(); 578 } 579 } 580 updateBubblePositions()581 private void updateBubblePositions() { 582 if (mAnimatingExpand || mAnimatingCollapse) { 583 return; 584 } 585 586 for (int i = 0; i < mLayout.getChildCount(); i++) { 587 final View bubble = mLayout.getChildAt(i); 588 589 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It 590 // will be snapped to the correct X value after the drag (if it's not dismissed). 591 if (bubble.equals(getDraggedOutBubble())) { 592 return; 593 } 594 595 animationForChild(bubble) 596 .translationX(getBubbleLeft(i)) 597 .start(); 598 } 599 } 600 601 /** 602 * @param index Bubble index in row. 603 * @return Bubble left x from left edge of screen. 604 */ getBubbleLeft(int index)605 public float getBubbleLeft(int index) { 606 final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles); 607 return getRowLeft() + bubbleFromRowLeft; 608 } 609 610 /** 611 * When expanded, the bubbles are centered in the screen. In portrait, all available space is 612 * used. In landscape we have too much space so the value is restricted. This method accounts 613 * for window decorations (nav bar, cutouts). 614 * 615 * @return the desired width to display the expanded bubbles in. 616 */ getWidthForDisplayingBubbles()617 public float getWidthForDisplayingBubbles() { 618 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */); 619 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) { 620 // display size y in landscape will be the smaller dimension of the screen 621 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT); 622 } else { 623 return availableWidth; 624 } 625 } 626 627 /** 628 * Determines the available screen width without the cutout. 629 * 630 * @param subtractStableInsets Whether or not stable insets should also be removed from the 631 * returned width. 632 * @return the total screen width available accounting for cutouts and insets, 633 * iff {@param includeStableInsets} is true. 634 */ getAvailableScreenWidth(boolean subtractStableInsets)635 private float getAvailableScreenWidth(boolean subtractStableInsets) { 636 float availableSize = mDisplaySize.x; 637 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null; 638 if (insets != null) { 639 int cutoutLeft = 0; 640 int cutoutRight = 0; 641 DisplayCutout cutout = insets.getDisplayCutout(); 642 if (cutout != null) { 643 cutoutLeft = cutout.getSafeInsetLeft(); 644 cutoutRight = cutout.getSafeInsetRight(); 645 } 646 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0; 647 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0; 648 availableSize -= Math.max(stableLeft, cutoutLeft); 649 availableSize -= Math.max(stableRight, cutoutRight); 650 } 651 return availableSize; 652 } 653 getRowLeft()654 private float getRowLeft() { 655 if (mLayout == null) { 656 return 0; 657 } 658 float rowWidth = (mLayout.getChildCount() * mBubbleSizePx) 659 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles); 660 661 // This display size we're using includes the size of the insets, we want the true 662 // center of the display minus the notch here, which means we should include the 663 // stable insets (e.g. status bar, nav bar) in this calculation. 664 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f; 665 return trueCenter - (rowWidth / 2f); 666 } 667 } 668