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