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