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.ContentResolver; 20 import android.content.res.Resources; 21 import android.graphics.PointF; 22 import android.graphics.Rect; 23 import android.graphics.RectF; 24 import android.provider.Settings; 25 import android.util.Log; 26 import android.view.View; 27 import android.view.WindowInsets; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 import androidx.dynamicanimation.animation.DynamicAnimation; 32 import androidx.dynamicanimation.animation.FlingAnimation; 33 import androidx.dynamicanimation.animation.FloatPropertyCompat; 34 import androidx.dynamicanimation.animation.SpringAnimation; 35 import androidx.dynamicanimation.animation.SpringForce; 36 37 import com.android.systemui.R; 38 import com.android.systemui.bubbles.BubbleStackView; 39 import com.android.systemui.util.FloatingContentCoordinator; 40 import com.android.systemui.util.animation.PhysicsAnimator; 41 import com.android.systemui.util.magnetictarget.MagnetizedObject; 42 43 import com.google.android.collect.Sets; 44 45 import java.io.FileDescriptor; 46 import java.io.PrintWriter; 47 import java.util.HashMap; 48 import java.util.Set; 49 import java.util.function.IntSupplier; 50 51 /** 52 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop 53 * each other with a slight offset to the left or right (depending on which side of the screen they 54 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of 55 * the screen. 56 */ 57 public class StackAnimationController extends 58 PhysicsAnimationLayout.PhysicsAnimationController { 59 60 private static final String TAG = "Bubbs.StackCtrl"; 61 62 /** Scale factor to use initially for new bubbles being animated in. */ 63 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f; 64 65 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */ 66 private static final int ANIMATE_TRANSLATION_FACTOR = 4; 67 68 /** Values to use for animating bubbles in. */ 69 private static final float ANIMATE_IN_STIFFNESS = 1000f; 70 private static final int ANIMATE_IN_START_DELAY = 25; 71 72 /** 73 * Values to use for the default {@link SpringForce} provided to the physics animation layout. 74 */ 75 public static final int DEFAULT_STIFFNESS = 12000; 76 public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW; 77 private static final int FLING_FOLLOW_STIFFNESS = 20000; 78 public static final float DEFAULT_BOUNCINESS = 0.9f; 79 80 private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = 81 new PhysicsAnimator.SpringConfig( 82 ANIMATE_IN_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); 83 84 /** 85 * Friction applied to fling animations. Since the stack must land on one of the sides of the 86 * screen, we want less friction horizontally so that the stack has a better chance of making it 87 * to the side without needing a spring. 88 */ 89 private static final float FLING_FRICTION = 2.2f; 90 91 /** 92 * Values to use for the stack spring animation used to spring the stack to its final position 93 * after a fling. 94 */ 95 private static final int SPRING_AFTER_FLING_STIFFNESS = 750; 96 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; 97 98 /** Sentinel value for unset position value. */ 99 private static final float UNSET = -Float.MIN_VALUE; 100 101 /** 102 * Minimum fling velocity required to trigger moving the stack from one side of the screen to 103 * the other. 104 */ 105 private static final float ESCAPE_VELOCITY = 750f; 106 107 /** Velocity required to dismiss the stack without dragging it into the dismiss target. */ 108 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f; 109 110 /** 111 * The canonical position of the stack. This is typically the position of the first bubble, but 112 * we need to keep track of it separately from the first bubble's translation in case there are 113 * no bubbles, or the first bubble was just added and being animated to its new position. 114 */ 115 private PointF mStackPosition = new PointF(-1, -1); 116 117 /** 118 * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic 119 * dismiss target. 120 */ 121 private MagnetizedObject<StackAnimationController> mMagnetizedStack; 122 123 /** 124 * The area that Bubbles will occupy after all animations end. This is used to move other 125 * floating content out of the way proactively. 126 */ 127 private Rect mAnimatingToBounds = new Rect(); 128 129 /** Initial starting location for the stack. */ 130 @Nullable private BubbleStackView.RelativeStackPosition mStackStartPosition; 131 132 /** Whether or not the stack's start position has been set. */ 133 private boolean mStackMovedToStartPosition = false; 134 135 /** 136 * The stack's most recent position along the edge of the screen. This is saved when the last 137 * bubble is removed, so that the stack can be restored in its previous position. 138 */ 139 private PointF mRestingStackPosition; 140 141 /** The height of the most recently visible IME. */ 142 private float mImeHeight = 0f; 143 144 /** 145 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the 146 * IME is not visible or the user moved the stack since the IME became visible. 147 */ 148 private float mPreImeY = UNSET; 149 150 /** 151 * Animations on the stack position itself, which would have been started in 152 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to 153 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect) 154 * to a legal position on the side of the screen. 155 */ 156 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations = 157 new HashMap<>(); 158 159 /** 160 * Whether the current motion of the stack is due to a fling animation (vs. being dragged 161 * manually). 162 */ 163 private boolean mIsMovingFromFlinging = false; 164 165 /** 166 * Whether the first bubble is springing towards the touch point, rather than using the default 167 * behavior of moving directly to the touch point with the rest of the stack following it. 168 * 169 * This happens when the user's finger exits the dismiss area while the stack is magnetized to 170 * the center. Since the touch point differs from the stack location, we need to animate the 171 * stack back to the touch point to avoid a jarring instant location change from the center of 172 * the target to the touch point just outside the target bounds. 173 * 174 * This is reset once the spring animations end, since that means the first bubble has 175 * successfully 'caught up' to the touch. 176 */ 177 private boolean mFirstBubbleSpringingToTouch = false; 178 179 /** 180 * Whether to spring the stack to the next touch event coordinates. This is used to animate the 181 * stack (including the first bubble) out of the magnetic dismiss target to the touch location. 182 * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly 183 * and only animating the following bubbles. 184 */ 185 private boolean mSpringToTouchOnNextMotionEvent = false; 186 187 /** Horizontal offset of bubbles in the stack. */ 188 private float mStackOffset; 189 /** Diameter of the bubble icon. */ 190 private int mBubbleBitmapSize; 191 /** Width of the bubble (icon and padding). */ 192 private int mBubbleSize; 193 /** 194 * The amount of space to add between the bubbles and certain UI elements, such as the top of 195 * the screen or the IME. This does not apply to the left/right sides of the screen since the 196 * stack goes offscreen intentionally. 197 */ 198 private int mBubblePaddingTop; 199 /** How far offscreen the stack rests. */ 200 private int mBubbleOffscreen; 201 /** How far down the screen the stack starts, when there is no pre-existing location. */ 202 private int mStackStartingVerticalOffset; 203 /** Height of the status bar. */ 204 private float mStatusBarHeight; 205 206 /** FloatingContentCoordinator instance for resolving floating content conflicts. */ 207 private FloatingContentCoordinator mFloatingContentCoordinator; 208 209 /** 210 * FloatingContent instance that returns the stack's location on the screen, and moves it when 211 * requested. 212 */ 213 private final FloatingContentCoordinator.FloatingContent mStackFloatingContent = 214 new FloatingContentCoordinator.FloatingContent() { 215 216 private final Rect mFloatingBoundsOnScreen = new Rect(); 217 218 @Override 219 public void moveToBounds(@NonNull Rect bounds) { 220 springStack(bounds.left, bounds.top, SpringForce.STIFFNESS_LOW); 221 } 222 223 @NonNull 224 @Override 225 public Rect getAllowedFloatingBoundsRegion() { 226 final Rect floatingBounds = getFloatingBoundsOnScreen(); 227 final Rect allowableStackArea = new Rect(); 228 getAllowableStackPositionRegion().roundOut(allowableStackArea); 229 allowableStackArea.right += floatingBounds.width(); 230 allowableStackArea.bottom += floatingBounds.height(); 231 return allowableStackArea; 232 } 233 234 @NonNull 235 @Override 236 public Rect getFloatingBoundsOnScreen() { 237 if (!mAnimatingToBounds.isEmpty()) { 238 return mAnimatingToBounds; 239 } 240 241 if (mLayout.getChildCount() > 0) { 242 // Calculate the bounds using stack position + bubble size so that we don't need to 243 // wait for the bubble views to lay out. 244 mFloatingBoundsOnScreen.set( 245 (int) mStackPosition.x, 246 (int) mStackPosition.y, 247 (int) mStackPosition.x + mBubbleSize, 248 (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop); 249 } else { 250 mFloatingBoundsOnScreen.setEmpty(); 251 } 252 253 return mFloatingBoundsOnScreen; 254 } 255 }; 256 257 /** Returns the number of 'real' bubbles (excluding the overflow bubble). */ 258 private IntSupplier mBubbleCountSupplier; 259 260 /** 261 * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the 262 * end of this animation means we have no bubbles left, and notify the BubbleController. 263 */ 264 private Runnable mOnBubbleAnimatedOutAction; 265 StackAnimationController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, Runnable onBubbleAnimatedOutAction)266 public StackAnimationController( 267 FloatingContentCoordinator floatingContentCoordinator, 268 IntSupplier bubbleCountSupplier, 269 Runnable onBubbleAnimatedOutAction) { 270 mFloatingContentCoordinator = floatingContentCoordinator; 271 mBubbleCountSupplier = bubbleCountSupplier; 272 mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; 273 } 274 275 /** 276 * Instantly move the first bubble to the given point, and animate the rest of the stack behind 277 * it with the 'following' effect. 278 */ moveFirstBubbleWithStackFollowing(float x, float y)279 public void moveFirstBubbleWithStackFollowing(float x, float y) { 280 // If we're moving the bubble around, we're not animating to any bounds. 281 mAnimatingToBounds.setEmpty(); 282 283 // If we manually move the bubbles with the IME open, clear the return point since we don't 284 // want the stack to snap away from the new position. 285 mPreImeY = UNSET; 286 287 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x); 288 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y); 289 290 // This method is called when the stack is being dragged manually, so we're clearly no 291 // longer flinging. 292 mIsMovingFromFlinging = false; 293 } 294 295 /** 296 * The position of the stack - typically the position of the first bubble; if no bubbles have 297 * been added yet, it will be where the first bubble will go when added. 298 */ getStackPosition()299 public PointF getStackPosition() { 300 return mStackPosition; 301 } 302 303 /** Whether the stack is on the left side of the screen. */ isStackOnLeftSide()304 public boolean isStackOnLeftSide() { 305 if (mLayout == null || !isStackPositionSet()) { 306 return true; // Default to left, which is where it starts by default. 307 } 308 309 float stackCenter = mStackPosition.x + mBubbleBitmapSize / 2; 310 float screenCenter = mLayout.getWidth() / 2; 311 return stackCenter < screenCenter; 312 } 313 314 /** 315 * Fling stack to given corner, within allowable screen bounds. 316 * Note that we need new SpringForce instances per animation despite identical configs because 317 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs. 318 */ springStack( float destinationX, float destinationY, float stiffness)319 public void springStack( 320 float destinationX, float destinationY, float stiffness) { 321 notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY); 322 323 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, 324 new SpringForce() 325 .setStiffness(stiffness) 326 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 327 0 /* startXVelocity */, 328 destinationX); 329 330 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, 331 new SpringForce() 332 .setStiffness(stiffness) 333 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), 334 0 /* startYVelocity */, 335 destinationY); 336 } 337 338 /** 339 * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after 340 * flings. 341 */ springStackAfterFling(float destinationX, float destinationY)342 public void springStackAfterFling(float destinationX, float destinationY) { 343 springStack(destinationX, destinationY, SPRING_AFTER_FLING_STIFFNESS); 344 } 345 346 /** 347 * Flings the stack starting with the given velocities, springing it to the nearest edge 348 * afterward. 349 * 350 * @return The X value that the stack will end up at after the fling/spring. 351 */ flingStackThenSpringToEdge(float x, float velX, float velY)352 public float flingStackThenSpringToEdge(float x, float velX, float velY) { 353 final boolean stackOnLeftSide = x - mBubbleBitmapSize / 2 < mLayout.getWidth() / 2; 354 355 final boolean stackShouldFlingLeft = stackOnLeftSide 356 ? velX < ESCAPE_VELOCITY 357 : velX < -ESCAPE_VELOCITY; 358 359 final RectF stackBounds = getAllowableStackPositionRegion(); 360 361 // Target X translation (either the left or right side of the screen). 362 final float destinationRelativeX = stackShouldFlingLeft 363 ? stackBounds.left : stackBounds.right; 364 365 // If all bubbles were removed during a drag event, just return the X we would have animated 366 // to if there were still bubbles. 367 if (mLayout == null || mLayout.getChildCount() == 0) { 368 return destinationRelativeX; 369 } 370 371 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 372 final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", 373 SPRING_AFTER_FLING_STIFFNESS /* default */); 374 final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", 375 SPRING_AFTER_FLING_DAMPING_RATIO); 376 final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction", 377 FLING_FRICTION); 378 379 // Minimum velocity required for the stack to make it to the targeted side of the screen, 380 // taking friction into account (4.2f is the number that friction scalars are multiplied by 381 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, 382 // but the SpringAnimation at the end will ensure that it reaches the destination X 383 // regardless. 384 final float minimumVelocityToReachEdge = 385 (destinationRelativeX - x) * (friction * 4.2f); 386 387 final float estimatedY = PhysicsAnimator.estimateFlingEndValue( 388 mStackPosition.y, velY, 389 new PhysicsAnimator.FlingConfig( 390 friction, stackBounds.top, stackBounds.bottom)); 391 392 notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY); 393 394 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so 395 // that it'll make it all the way to the side of the screen. 396 final float startXVelocity = stackShouldFlingLeft 397 ? Math.min(minimumVelocityToReachEdge, velX) 398 : Math.max(minimumVelocityToReachEdge, velX); 399 400 401 402 flingThenSpringFirstBubbleWithStackFollowing( 403 DynamicAnimation.TRANSLATION_X, 404 startXVelocity, 405 friction, 406 new SpringForce() 407 .setStiffness(stiffness) 408 .setDampingRatio(dampingRatio), 409 destinationRelativeX); 410 411 flingThenSpringFirstBubbleWithStackFollowing( 412 DynamicAnimation.TRANSLATION_Y, 413 velY, 414 friction, 415 new SpringForce() 416 .setStiffness(stiffness) 417 .setDampingRatio(dampingRatio), 418 /* destination */ null); 419 420 // If we're flinging now, there's no more touch event to catch up to. 421 mFirstBubbleSpringingToTouch = false; 422 mIsMovingFromFlinging = true; 423 return destinationRelativeX; 424 } 425 426 /** 427 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). 428 */ 429 public PointF getStackPositionAlongNearestHorizontalEdge() { 430 final PointF stackPos = getStackPosition(); 431 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); 432 final RectF bounds = getAllowableStackPositionRegion(); 433 434 stackPos.x = onLeft ? bounds.left : bounds.right; 435 return stackPos; 436 } 437 438 /** Description of current animation controller state. */ 439 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 440 pw.println("StackAnimationController state:"); 441 pw.print(" isActive: "); pw.println(isActiveController()); 442 pw.print(" restingStackPos: "); 443 pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null"); 444 pw.print(" currentStackPos: "); pw.println(mStackPosition.toString()); 445 pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging); 446 pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget()); 447 pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch); 448 } 449 450 /** 451 * Flings the first bubble along the given property's axis, using the provided configuration 452 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently 453 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final 454 * position. 455 */ 456 protected void flingThenSpringFirstBubbleWithStackFollowing( 457 DynamicAnimation.ViewProperty property, 458 float vel, 459 float friction, 460 SpringForce spring, 461 Float finalPosition) { 462 if (!isActiveController()) { 463 return; 464 } 465 466 Log.d(TAG, String.format("Flinging %s.", 467 PhysicsAnimationLayout.getReadablePropertyName(property))); 468 469 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 470 final float currentValue = firstBubbleProperty.getValue(this); 471 final RectF bounds = getAllowableStackPositionRegion(); 472 final float min = 473 property.equals(DynamicAnimation.TRANSLATION_X) 474 ? bounds.left 475 : bounds.top; 476 final float max = 477 property.equals(DynamicAnimation.TRANSLATION_X) 478 ? bounds.right 479 : bounds.bottom; 480 481 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty); 482 flingAnimation.setFriction(friction) 483 .setStartVelocity(vel) 484 485 // If the bubble's property value starts beyond the desired min/max, use that value 486 // instead so that the animation won't immediately end. If, for example, the user 487 // drags the bubbles into the navigation bar, but then flings them upward, we want 488 // the fling to occur despite temporarily having a value outside of the min/max. If 489 // the bubbles are out of bounds and flung even farther out of bounds, the fling 490 // animation will halt immediately and the SpringAnimation will take over, springing 491 // it in reverse to the (legal) final position. 492 .setMinValue(Math.min(currentValue, min)) 493 .setMaxValue(Math.max(currentValue, max)) 494 495 .addEndListener((animation, canceled, endValue, endVelocity) -> { 496 if (!canceled) { 497 mRestingStackPosition.set(mStackPosition); 498 499 springFirstBubbleWithStackFollowing(property, spring, endVelocity, 500 finalPosition != null 501 ? finalPosition 502 : Math.max(min, Math.min(max, endValue))); 503 } 504 }); 505 506 cancelStackPositionAnimation(property); 507 mStackPositionAnimations.put(property, flingAnimation); 508 flingAnimation.start(); 509 } 510 511 /** 512 * Cancel any stack position animations that were started by calling 513 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end 514 * listeners. 515 */ 516 public void cancelStackPositionAnimations() { 517 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); 518 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); 519 520 removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); 521 removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); 522 } 523 524 /** Save the current IME height so that we know where the stack bounds should be. */ 525 public void setImeHeight(int imeHeight) { 526 mImeHeight = imeHeight; 527 } 528 529 /** 530 * Animates the stack either away from the newly visible IME, or back to its original position 531 * due to the IME going away. 532 * 533 * @return The destination Y value of the stack due to the IME movement (or the current position 534 * of the stack if it's not moving). 535 */ 536 public float animateForImeVisibility(boolean imeVisible) { 537 final float maxBubbleY = getAllowableStackPositionRegion().bottom; 538 float destinationY = UNSET; 539 540 if (imeVisible) { 541 // Stack is lower than it should be and overlaps the now-visible IME. 542 if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) { 543 mPreImeY = mStackPosition.y; 544 destinationY = maxBubbleY; 545 } 546 } else { 547 if (mPreImeY != UNSET) { 548 destinationY = mPreImeY; 549 mPreImeY = UNSET; 550 } 551 } 552 553 if (destinationY != UNSET) { 554 springFirstBubbleWithStackFollowing( 555 DynamicAnimation.TRANSLATION_Y, 556 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null) 557 .setStiffness(IME_ANIMATION_STIFFNESS), 558 /* startVel */ 0f, 559 destinationY); 560 561 notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY); 562 } 563 564 return destinationY != UNSET ? destinationY : mStackPosition.y; 565 } 566 567 /** 568 * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so 569 * we return these bounds from 570 * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. 571 */ 572 private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) { 573 final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen(); 574 floatingBounds.offsetTo((int) x, (int) y); 575 mAnimatingToBounds = floatingBounds; 576 mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); 577 } 578 579 /** 580 * Returns the region that the stack position must stay within. This goes slightly off the left 581 * and right sides of the screen, below the status bar/cutout and above the navigation bar. 582 * While the stack position is not allowed to rest outside of these bounds, it can temporarily 583 * be animated or dragged beyond them. 584 */ 585 public RectF getAllowableStackPositionRegion() { 586 final WindowInsets insets = mLayout.getRootWindowInsets(); 587 final RectF allowableRegion = new RectF(); 588 if (insets != null) { 589 allowableRegion.left = 590 -mBubbleOffscreen 591 + Math.max( 592 insets.getSystemWindowInsetLeft(), 593 insets.getDisplayCutout() != null 594 ? insets.getDisplayCutout().getSafeInsetLeft() 595 : 0); 596 allowableRegion.right = 597 mLayout.getWidth() 598 - mBubbleSize 599 + mBubbleOffscreen 600 - Math.max( 601 insets.getSystemWindowInsetRight(), 602 insets.getDisplayCutout() != null 603 ? insets.getDisplayCutout().getSafeInsetRight() 604 : 0); 605 606 allowableRegion.top = 607 mBubblePaddingTop 608 + Math.max( 609 mStatusBarHeight, 610 insets.getDisplayCutout() != null 611 ? insets.getDisplayCutout().getSafeInsetTop() 612 : 0); 613 allowableRegion.bottom = 614 mLayout.getHeight() 615 - mBubbleSize 616 - mBubblePaddingTop 617 - (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f) 618 - Math.max( 619 insets.getStableInsetBottom(), 620 insets.getDisplayCutout() != null 621 ? insets.getDisplayCutout().getSafeInsetBottom() 622 : 0); 623 } 624 625 return allowableRegion; 626 } 627 628 /** Moves the stack in response to a touch event. */ 629 public void moveStackFromTouch(float x, float y) { 630 // Begin the spring-to-touch catch up animation if needed. 631 if (mSpringToTouchOnNextMotionEvent) { 632 springStack(x, y, DEFAULT_STIFFNESS); 633 mSpringToTouchOnNextMotionEvent = false; 634 mFirstBubbleSpringingToTouch = true; 635 } else if (mFirstBubbleSpringingToTouch) { 636 final SpringAnimation springToTouchX = 637 (SpringAnimation) mStackPositionAnimations.get( 638 DynamicAnimation.TRANSLATION_X); 639 final SpringAnimation springToTouchY = 640 (SpringAnimation) mStackPositionAnimations.get( 641 DynamicAnimation.TRANSLATION_Y); 642 643 // If either animation is still running, we haven't caught up. Update the animations. 644 if (springToTouchX.isRunning() || springToTouchY.isRunning()) { 645 springToTouchX.animateToFinalPosition(x); 646 springToTouchY.animateToFinalPosition(y); 647 } else { 648 // If the animations have finished, the stack is now at the touch point. We can 649 // resume moving the bubble directly. 650 mFirstBubbleSpringingToTouch = false; 651 } 652 } 653 654 if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) { 655 moveFirstBubbleWithStackFollowing(x, y); 656 } 657 } 658 659 /** Notify the controller that the stack has been unstuck from the dismiss target. */ 660 public void onUnstuckFromTarget() { 661 mSpringToTouchOnNextMotionEvent = true; 662 } 663 664 /** 665 * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down. 666 */ 667 public void animateStackDismissal(float translationYBy, Runnable after) { 668 animationsForChildrenFromIndex(0, (index, animation) -> 669 animation 670 .scaleX(0f) 671 .scaleY(0f) 672 .alpha(0f) 673 .translationY( 674 mLayout.getChildAt(index).getTranslationY() + translationYBy) 675 .withStiffness(SpringForce.STIFFNESS_HIGH)) 676 .startAll(after); 677 } 678 679 /** 680 * Springs the first bubble to the given final position, with the rest of the stack 'following'. 681 */ springFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, SpringForce spring, float vel, float finalPosition, @Nullable Runnable... after)682 protected void springFirstBubbleWithStackFollowing( 683 DynamicAnimation.ViewProperty property, SpringForce spring, 684 float vel, float finalPosition, @Nullable Runnable... after) { 685 686 if (mLayout.getChildCount() == 0 || !isActiveController()) { 687 return; 688 } 689 690 Log.d(TAG, String.format("Springing %s to final position %f.", 691 PhysicsAnimationLayout.getReadablePropertyName(property), 692 finalPosition)); 693 694 // Whether we're springing towards the touch location, rather than to a position on the 695 // sides of the screen. 696 final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent; 697 698 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); 699 SpringAnimation springAnimation = 700 new SpringAnimation(this, firstBubbleProperty) 701 .setSpring(spring) 702 .addEndListener((dynamicAnimation, b, v, v1) -> { 703 if (!isSpringingTowardsTouch) { 704 // If we're springing towards the touch position, don't save the 705 // resting position - the touch location is not a valid resting 706 // position. We'll set this when the stack springs to the left or 707 // right side of the screen after the touch gesture ends. 708 mRestingStackPosition.set(mStackPosition); 709 } 710 711 if (after != null) { 712 for (Runnable callback : after) { 713 callback.run(); 714 } 715 } 716 }) 717 .setStartVelocity(vel); 718 719 cancelStackPositionAnimation(property); 720 mStackPositionAnimations.put(property, springAnimation); 721 springAnimation.animateToFinalPosition(finalPosition); 722 } 723 724 @Override getAnimatedProperties()725 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 726 return Sets.newHashSet( 727 DynamicAnimation.TRANSLATION_X, // For positioning. 728 DynamicAnimation.TRANSLATION_Y, 729 DynamicAnimation.ALPHA, // For fading in new bubbles. 730 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles. 731 DynamicAnimation.SCALE_Y); 732 } 733 734 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)735 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 736 if (property.equals(DynamicAnimation.TRANSLATION_X) 737 || property.equals(DynamicAnimation.TRANSLATION_Y)) { 738 return index + 1; 739 } else { 740 return NONE; 741 } 742 } 743 744 745 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)746 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { 747 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 748 // If we're in the dismiss target, have the bubbles pile on top of each other with no 749 // offset. 750 if (isStackStuckToTarget()) { 751 return 0f; 752 } else { 753 // Offset to the left if we're on the left, or the right otherwise. 754 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x) 755 ? -mStackOffset : mStackOffset; 756 } 757 } else { 758 return 0f; 759 } 760 } 761 762 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)763 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 764 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 765 final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", 766 mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS /* default */); 767 final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", 768 DEFAULT_BOUNCINESS); 769 770 return new SpringForce() 771 .setDampingRatio(dampingRatio) 772 .setStiffness(stiffness); 773 } 774 775 @Override onChildAdded(View child, int index)776 void onChildAdded(View child, int index) { 777 // Don't animate additions within the dismiss target. 778 if (isStackStuckToTarget()) { 779 return; 780 } 781 782 if (getBubbleCount() == 1) { 783 // If this is the first child added, position the stack in its starting position. 784 moveStackToStartPosition(); 785 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { 786 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble 787 // to the back of the stack, it'll be largely invisible so don't bother animating it in. 788 animateInBubble(child, index); 789 } 790 } 791 792 @Override onChildRemoved(View child, int index, Runnable finishRemoval)793 void onChildRemoved(View child, int index, Runnable finishRemoval) { 794 PhysicsAnimator.getInstance(child) 795 .spring(DynamicAnimation.ALPHA, 0f) 796 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) 797 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) 798 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) 799 .start(); 800 801 // If there are other bubbles, pull them into the correct position. 802 if (getBubbleCount() > 0) { 803 animationForChildAtIndex(0).translationX(mStackPosition.x).start(); 804 } else { 805 // When all children are removed ensure stack position is sane 806 setStackPosition(mRestingStackPosition == null 807 ? getStartPosition() 808 : mRestingStackPosition); 809 810 // Remove the stack from the coordinator since we don't have any bubbles and aren't 811 // visible. 812 mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent); 813 } 814 } 815 816 @Override onChildReordered(View child, int oldIndex, int newIndex)817 void onChildReordered(View child, int oldIndex, int newIndex) { 818 if (isStackPositionSet()) { 819 setStackPosition(mStackPosition); 820 } 821 } 822 823 @Override onActiveControllerForLayout(PhysicsAnimationLayout layout)824 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 825 Resources res = layout.getResources(); 826 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 827 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 828 mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size); 829 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 830 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); 831 mStackStartingVerticalOffset = 832 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); 833 mStatusBarHeight = 834 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 835 } 836 837 /** 838 * Update effective screen width based on current orientation. 839 * @param orientation Landscape or portrait. 840 */ updateResources(int orientation)841 public void updateResources(int orientation) { 842 if (mLayout != null) { 843 Resources res = mLayout.getContext().getResources(); 844 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 845 mStatusBarHeight = res.getDimensionPixelSize( 846 com.android.internal.R.dimen.status_bar_height); 847 } 848 } 849 isStackStuckToTarget()850 private boolean isStackStuckToTarget() { 851 return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget(); 852 } 853 854 /** Moves the stack, without any animation, to the starting position. */ moveStackToStartPosition()855 private void moveStackToStartPosition() { 856 // Post to ensure that the layout's width and height have been calculated. 857 mLayout.setVisibility(View.INVISIBLE); 858 mLayout.post(() -> { 859 setStackPosition(mRestingStackPosition == null 860 ? getStartPosition() 861 : mRestingStackPosition); 862 mStackMovedToStartPosition = true; 863 mLayout.setVisibility(View.VISIBLE); 864 865 // Animate in the top bubble now that we're visible. 866 if (mLayout.getChildCount() > 0) { 867 // Add the stack to the floating content coordinator now that we have a bubble and 868 // are visible. 869 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent); 870 871 animateInBubble(mLayout.getChildAt(0), 0 /* index */); 872 } 873 }); 874 } 875 876 /** 877 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent 878 * bubbles to animate 'following' to the new location. 879 */ moveFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, float value)880 private void moveFirstBubbleWithStackFollowing( 881 DynamicAnimation.ViewProperty property, float value) { 882 883 // Update the canonical stack position. 884 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 885 mStackPosition.x = value; 886 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 887 mStackPosition.y = value; 888 } 889 890 if (mLayout.getChildCount() > 0) { 891 property.setValue(mLayout.getChildAt(0), value); 892 if (mLayout.getChildCount() > 1) { 893 animationForChildAtIndex(1) 894 .property(property, value + getOffsetForChainedPropertyAnimation(property)) 895 .start(); 896 } 897 } 898 } 899 900 /** Moves the stack to a position instantly, with no animation. */ setStackPosition(PointF pos)901 public void setStackPosition(PointF pos) { 902 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); 903 mStackPosition.set(pos.x, pos.y); 904 905 if (mRestingStackPosition == null) { 906 mRestingStackPosition = new PointF(); 907 } 908 909 mRestingStackPosition.set(mStackPosition); 910 911 // If we're not the active controller, we don't want to physically move the bubble views. 912 if (isActiveController()) { 913 // Cancel animations that could be moving the views. 914 mLayout.cancelAllAnimationsOfProperties( 915 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); 916 cancelStackPositionAnimations(); 917 918 // Since we're not using the chained animations, apply the offsets manually. 919 final float xOffset = getOffsetForChainedPropertyAnimation( 920 DynamicAnimation.TRANSLATION_X); 921 final float yOffset = getOffsetForChainedPropertyAnimation( 922 DynamicAnimation.TRANSLATION_Y); 923 for (int i = 0; i < mLayout.getChildCount(); i++) { 924 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); 925 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); 926 } 927 } 928 } 929 setStackPosition(BubbleStackView.RelativeStackPosition position)930 public void setStackPosition(BubbleStackView.RelativeStackPosition position) { 931 setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); 932 } 933 getRelativeStackPosition()934 public BubbleStackView.RelativeStackPosition getRelativeStackPosition() { 935 return new BubbleStackView.RelativeStackPosition( 936 mStackPosition, getAllowableStackPositionRegion()); 937 } 938 939 /** 940 * Sets the starting position for the stack, where it will be located when the first bubble is 941 * added. 942 */ setStackStartPosition(BubbleStackView.RelativeStackPosition position)943 public void setStackStartPosition(BubbleStackView.RelativeStackPosition position) { 944 mStackStartPosition = position; 945 } 946 947 /** 948 * Returns the starting stack position. If {@link #setStackStartPosition} was called, this will 949 * return that position - otherwise, a reasonable default will be returned. 950 */ getStartPosition()951 @Nullable public PointF getStartPosition() { 952 if (mLayout == null) { 953 return null; 954 } 955 956 if (mStackStartPosition == null) { 957 // Start on the left if we're in LTR, right otherwise. 958 final boolean startOnLeft = 959 mLayout.getResources().getConfiguration().getLayoutDirection() 960 != View.LAYOUT_DIRECTION_RTL; 961 962 final float startingVerticalOffset = mLayout.getResources().getDimensionPixelOffset( 963 R.dimen.bubble_stack_starting_offset_y); 964 965 mStackStartPosition = new BubbleStackView.RelativeStackPosition( 966 startOnLeft, 967 startingVerticalOffset / getAllowableStackPositionRegion().height()); 968 } 969 970 return mStackStartPosition.getAbsolutePositionInRegion(getAllowableStackPositionRegion()); 971 } 972 isStackPositionSet()973 private boolean isStackPositionSet() { 974 return mStackMovedToStartPosition; 975 } 976 977 /** Animates in the given bubble. */ animateInBubble(View child, int index)978 private void animateInBubble(View child, int index) { 979 if (!isActiveController()) { 980 return; 981 } 982 983 final float xOffset = 984 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); 985 986 // Position the new bubble in the correct position, scaled down completely. 987 child.setTranslationX(mStackPosition.x + xOffset * index); 988 child.setTranslationY(mStackPosition.y); 989 child.setScaleX(0f); 990 child.setScaleY(0f); 991 992 // Push the subsequent views out of the way, if there are subsequent views. 993 if (index + 1 < mLayout.getChildCount()) { 994 animationForChildAtIndex(index + 1) 995 .translationX(mStackPosition.x + xOffset * (index + 1)) 996 .withStiffness(SpringForce.STIFFNESS_LOW) 997 .start(); 998 } 999 1000 // Scale in the new bubble, slightly delayed. 1001 animationForChild(child) 1002 .scaleX(1f) 1003 .scaleY(1f) 1004 .withStiffness(ANIMATE_IN_STIFFNESS) 1005 .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0) 1006 .start(); 1007 } 1008 1009 /** 1010 * Cancels any outstanding first bubble property animations that are running. This does not 1011 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only 1012 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and 1013 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. 1014 */ cancelStackPositionAnimation(DynamicAnimation.ViewProperty property)1015 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) { 1016 if (mStackPositionAnimations.containsKey(property)) { 1017 mStackPositionAnimations.get(property).cancel(); 1018 } 1019 } 1020 1021 /** 1022 * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided 1023 * {@link MagnetizedObject.MagneticTarget} added as a target. 1024 */ getMagnetizedStack( MagnetizedObject.MagneticTarget target)1025 public MagnetizedObject<StackAnimationController> getMagnetizedStack( 1026 MagnetizedObject.MagneticTarget target) { 1027 if (mMagnetizedStack == null) { 1028 mMagnetizedStack = new MagnetizedObject<StackAnimationController>( 1029 mLayout.getContext(), 1030 this, 1031 new StackPositionProperty(DynamicAnimation.TRANSLATION_X), 1032 new StackPositionProperty(DynamicAnimation.TRANSLATION_Y) 1033 ) { 1034 @Override 1035 public float getWidth(@NonNull StackAnimationController underlyingObject) { 1036 return mBubbleSize; 1037 } 1038 1039 @Override 1040 public float getHeight(@NonNull StackAnimationController underlyingObject) { 1041 return mBubbleSize; 1042 } 1043 1044 @Override 1045 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject, 1046 @NonNull int[] loc) { 1047 loc[0] = (int) mStackPosition.x; 1048 loc[1] = (int) mStackPosition.y; 1049 } 1050 }; 1051 mMagnetizedStack.addTarget(target); 1052 mMagnetizedStack.setHapticsEnabled(true); 1053 mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); 1054 } 1055 1056 final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); 1057 final float minVelocity = Settings.Secure.getFloat(contentResolver, 1058 "bubble_dismiss_fling_min_velocity", 1059 mMagnetizedStack.getFlingToTargetMinVelocity() /* default */); 1060 final float maxVelocity = Settings.Secure.getFloat(contentResolver, 1061 "bubble_dismiss_stick_max_velocity", 1062 mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */); 1063 final float targetWidth = Settings.Secure.getFloat(contentResolver, 1064 "bubble_dismiss_target_width_percent", 1065 mMagnetizedStack.getFlingToTargetWidthPercent() /* default */); 1066 1067 mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity); 1068 mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity); 1069 mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth); 1070 1071 return mMagnetizedStack; 1072 } 1073 1074 /** Returns the number of 'real' bubbles (excluding overflow). */ getBubbleCount()1075 private int getBubbleCount() { 1076 return mBubbleCountSupplier.getAsInt(); 1077 } 1078 1079 /** 1080 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's 1081 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this 1082 * property directly to move the first bubble and cause the stack to 'follow' to the new 1083 * location. 1084 * 1085 * This could also be achieved by simply animating the first bubble view and adding an update 1086 * listener to dispatch movement to the rest of the stack. However, this would require 1087 * duplication of logic in that update handler - it's simpler to keep all logic contained in the 1088 * {@link #moveFirstBubbleWithStackFollowing} method. 1089 */ 1090 private class StackPositionProperty 1091 extends FloatPropertyCompat<StackAnimationController> { 1092 private final DynamicAnimation.ViewProperty mProperty; 1093 StackPositionProperty(DynamicAnimation.ViewProperty property)1094 private StackPositionProperty(DynamicAnimation.ViewProperty property) { 1095 super(property.toString()); 1096 mProperty = property; 1097 } 1098 1099 @Override getValue(StackAnimationController controller)1100 public float getValue(StackAnimationController controller) { 1101 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0; 1102 } 1103 1104 @Override setValue(StackAnimationController controller, float value)1105 public void setValue(StackAnimationController controller, float value) { 1106 moveFirstBubbleWithStackFollowing(mProperty, value); 1107 } 1108 } 1109 } 1110 1111