1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.bubbles.animation; 18 19 import android.content.res.Resources; 20 import android.graphics.Point; 21 import android.graphics.PointF; 22 import android.view.View; 23 import android.view.WindowInsets; 24 25 import androidx.annotation.Nullable; 26 import androidx.dynamicanimation.animation.DynamicAnimation; 27 import androidx.dynamicanimation.animation.SpringForce; 28 29 import com.android.systemui.R; 30 31 import com.google.android.collect.Sets; 32 33 import java.util.Set; 34 35 /** 36 * Animation controller for bubbles when they're in their expanded state, or animating to/from the 37 * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be 38 * dismissed. 39 */ 40 public class ExpandedAnimationController 41 extends PhysicsAnimationLayout.PhysicsAnimationController { 42 43 /** 44 * How much to translate the bubbles when they're animating in/out. This value is multiplied by 45 * the bubble size. 46 */ 47 private static final int ANIMATE_TRANSLATION_FACTOR = 4; 48 49 /** How much to scale down bubbles when they're animating in/out. */ 50 private static final float ANIMATE_SCALE_PERCENT = 0.5f; 51 52 /** The stack position to collapse back to in {@link #collapseBackToStack}. */ 53 private PointF mCollapseToPoint; 54 55 /** Horizontal offset between bubbles, which we need to know to re-stack them. */ 56 private float mStackOffsetPx; 57 /** Spacing between bubbles in the expanded state. */ 58 private float mBubblePaddingPx; 59 /** Size of each bubble. */ 60 private float mBubbleSizePx; 61 /** Height of the status bar. */ 62 private float mStatusBarHeight; 63 /** Size of display. */ 64 private Point mDisplaySize; 65 /** Size of dismiss target at bottom of screen. */ 66 private float mPipDismissHeight; 67 68 /** Whether the dragged-out bubble is in the dismiss target. */ 69 private boolean mIndividualBubbleWithinDismissTarget = false; 70 71 private boolean mAnimatingExpand = false; 72 private boolean mAnimatingCollapse = false; 73 private Runnable mAfterExpand; 74 private Runnable mAfterCollapse; 75 private PointF mCollapsePoint; 76 77 /** 78 * Whether the dragged out bubble is springing towards the touch point, rather than using the 79 * default behavior of moving directly to the touch point. 80 * 81 * This happens when the user's finger exits the dismiss area while the bubble is magnetized to 82 * the center. Since the touch point differs from the bubble location, we need to animate the 83 * bubble back to the touch point to avoid a jarring instant location change from the center of 84 * the target to the touch point just outside the target bounds. 85 */ 86 private boolean mSpringingBubbleToTouch = false; 87 88 private int mExpandedViewPadding; 89 ExpandedAnimationController(Point displaySize, int expandedViewPadding)90 public ExpandedAnimationController(Point displaySize, int expandedViewPadding) { 91 mDisplaySize = displaySize; 92 mExpandedViewPadding = expandedViewPadding; 93 } 94 95 /** 96 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause 97 * the rest of the bubbles to animate to fill the gap. 98 */ 99 private boolean mBubbleDraggedOutEnough = false; 100 101 /** The bubble currently being dragged out of the row (to potentially be dismissed). */ 102 private View mBubbleDraggingOut; 103 104 /** 105 * Animates expanding the bubbles into a row along the top of the screen. 106 */ expandFromStack(Runnable after)107 public void expandFromStack(Runnable after) { 108 mAnimatingCollapse = false; 109 mAnimatingExpand = true; 110 mAfterExpand = after; 111 112 startOrUpdateExpandAnimation(); 113 } 114 115 /** Animate collapsing the bubbles back to their stacked position. */ collapseBackToStack(PointF collapsePoint, Runnable after)116 public void collapseBackToStack(PointF collapsePoint, Runnable after) { 117 mAnimatingExpand = false; 118 mAnimatingCollapse = true; 119 mAfterCollapse = after; 120 mCollapsePoint = collapsePoint; 121 122 startOrUpdateCollapseAnimation(); 123 } 124 startOrUpdateExpandAnimation()125 private void startOrUpdateExpandAnimation() { 126 animationsForChildrenFromIndex( 127 0, /* startIndex */ 128 (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY())) 129 .startAll(() -> { 130 mAnimatingExpand = false; 131 132 if (mAfterExpand != null) { 133 mAfterExpand.run(); 134 } 135 136 mAfterExpand = null; 137 }); 138 } 139 startOrUpdateCollapseAnimation()140 private void startOrUpdateCollapseAnimation() { 141 // Stack to the left if we're going to the left, or right if not. 142 final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; 143 animationsForChildrenFromIndex( 144 0, /* startIndex */ 145 (index, animation) -> { 146 animation.position( 147 mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx), 148 mCollapsePoint.y); 149 }) 150 .startAll(() -> { 151 mAnimatingCollapse = false; 152 153 if (mAfterCollapse != null) { 154 mAfterCollapse.run(); 155 } 156 157 mAfterCollapse = null; 158 }); 159 } 160 161 /** Prepares the given bubble to be dragged out. */ prepareForBubbleDrag(View bubble)162 public void prepareForBubbleDrag(View bubble) { 163 mLayout.cancelAnimationsOnView(bubble); 164 165 mBubbleDraggingOut = bubble; 166 mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE); 167 } 168 169 /** 170 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to 171 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the 172 * bubble is dragged back into the row. 173 */ dragBubbleOut(View bubbleView, float x, float y)174 public void dragBubbleOut(View bubbleView, float x, float y) { 175 if (mSpringingBubbleToTouch) { 176 if (mLayout.arePropertiesAnimatingOnView( 177 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { 178 animationForChild(mBubbleDraggingOut) 179 .translationX(x) 180 .translationY(y) 181 .withStiffness(SpringForce.STIFFNESS_HIGH) 182 .start(); 183 } else { 184 mSpringingBubbleToTouch = false; 185 } 186 } 187 188 if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) { 189 bubbleView.setTranslationX(x); 190 bubbleView.setTranslationY(y); 191 } 192 193 final boolean draggedOutEnough = 194 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; 195 if (draggedOutEnough != mBubbleDraggedOutEnough) { 196 updateBubblePositions(); 197 mBubbleDraggedOutEnough = draggedOutEnough; 198 } 199 } 200 201 /** Plays a dismiss animation on the dragged out bubble. */ 202 public void dismissDraggedOutBubble(View bubble, Runnable after) { 203 mIndividualBubbleWithinDismissTarget = false; 204 205 animationForChild(bubble) 206 .withStiffness(SpringForce.STIFFNESS_HIGH) 207 .scaleX(1.1f) 208 .scaleY(1.1f) 209 .alpha(0f, after) 210 .start(); 211 212 updateBubblePositions(); 213 } 214 215 @Nullable public View getDraggedOutBubble() { 216 return mBubbleDraggingOut; 217 } 218 219 /** Magnets the given bubble to the dismiss target. */ 220 public void magnetBubbleToDismiss( 221 View bubbleView, float velX, float velY, float destY, Runnable after) { 222 mIndividualBubbleWithinDismissTarget = true; 223 mSpringingBubbleToTouch = false; 224 animationForChild(bubbleView) 225 .withStiffness(SpringForce.STIFFNESS_MEDIUM) 226 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 227 .withPositionStartVelocities(velX, velY) 228 .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f) 229 .translationY(destY, after) 230 .start(); 231 } 232 233 /** 234 * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch 235 * events update the spring's final position until it's settled. 236 */ 237 public void demagnetizeBubbleTo(float x, float y, float velX, float velY) { 238 mIndividualBubbleWithinDismissTarget = false; 239 mSpringingBubbleToTouch = true; 240 241 animationForChild(mBubbleDraggingOut) 242 .translationX(x) 243 .translationY(y) 244 .withPositionStartVelocities(velX, velY) 245 .withStiffness(SpringForce.STIFFNESS_HIGH) 246 .start(); 247 } 248 249 /** 250 * Snaps a bubble back to its position within the bubble row, and animates the rest of the 251 * bubbles to accommodate it if it was previously dragged out past the threshold. 252 */ 253 public void snapBubbleBack(View bubbleView, float velX, float velY) { 254 final int index = mLayout.indexOfChild(bubbleView); 255 256 animationForChildAtIndex(index) 257 .position(getBubbleLeft(index), getExpandedY()) 258 .withPositionStartVelocities(velX, velY) 259 .start(() -> bubbleView.setTranslationZ(0f) /* after */); 260 261 updateBubblePositions(); 262 } 263 264 /** Resets bubble drag out gesture flags. */ onGestureFinished()265 public void onGestureFinished() { 266 mBubbleDraggedOutEnough = false; 267 mBubbleDraggingOut = null; 268 } 269 270 /** 271 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. 272 */ updateYPosition(Runnable after)273 public void updateYPosition(Runnable after) { 274 if (mLayout == null) return; 275 animationsForChildrenFromIndex( 276 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); 277 } 278 279 /** 280 * Animates the bubbles, starting at the given index, to the left or right by the given number 281 * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal 282 * positions. 283 */ animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex)284 private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) { 285 animationsForChildrenFromIndex( 286 startIndex, 287 (index, animation) -> 288 animation.translationX(getXForChildAtIndex(index + numBubbleWidths))) 289 .startAll(); 290 } 291 292 /** The Y value of the row of expanded bubbles. */ getExpandedY()293 public float getExpandedY() { 294 if (mLayout == null || mLayout.getRootWindowInsets() == null) { 295 return 0; 296 } 297 final WindowInsets insets = mLayout.getRootWindowInsets(); 298 return mBubblePaddingPx + Math.max( 299 mStatusBarHeight, 300 insets.getDisplayCutout() != null 301 ? insets.getDisplayCutout().getSafeInsetTop() 302 : 0); 303 } 304 305 @Override onActiveControllerForLayout(PhysicsAnimationLayout layout)306 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 307 final Resources res = layout.getResources(); 308 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 309 mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding); 310 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 311 mStatusBarHeight = 312 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 313 mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height); 314 315 // Ensure that all child views are at 1x scale, and visible, in case they were animating 316 // in. 317 mLayout.setVisibility(View.VISIBLE); 318 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) -> 319 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll(); 320 } 321 322 @Override getAnimatedProperties()323 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 324 return Sets.newHashSet( 325 DynamicAnimation.TRANSLATION_X, 326 DynamicAnimation.TRANSLATION_Y, 327 DynamicAnimation.SCALE_X, 328 DynamicAnimation.SCALE_Y, 329 DynamicAnimation.ALPHA); 330 } 331 332 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)333 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 334 return NONE; 335 } 336 337 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)338 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { 339 return 0; 340 } 341 342 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)343 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 344 return new SpringForce() 345 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 346 .setStiffness(SpringForce.STIFFNESS_LOW); 347 } 348 349 @Override onChildAdded(View child, int index)350 void onChildAdded(View child, int index) { 351 // If a bubble is added while the expand/collapse animations are playing, update the 352 // animation to include the new bubble. 353 if (mAnimatingExpand) { 354 startOrUpdateExpandAnimation(); 355 } else if (mAnimatingCollapse) { 356 startOrUpdateCollapseAnimation(); 357 } else { 358 child.setTranslationX(getXForChildAtIndex(index)); 359 animationForChild(child) 360 .translationY( 361 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ 362 getExpandedY() /* to */) 363 .start(); 364 updateBubblePositions(); 365 } 366 } 367 368 @Override onChildRemoved(View child, int index, Runnable finishRemoval)369 void onChildRemoved(View child, int index, Runnable finishRemoval) { 370 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child); 371 372 // If we're removing the dragged-out bubble, that means it got dismissed. 373 if (child.equals(mBubbleDraggingOut)) { 374 mBubbleDraggingOut = null; 375 finishRemoval.run(); 376 } else { 377 animator.alpha(0f, finishRemoval /* endAction */) 378 .withStiffness(SpringForce.STIFFNESS_HIGH) 379 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) 380 .scaleX(1.1f) 381 .scaleY(1.1f) 382 .start(); 383 } 384 385 // Animate all the other bubbles to their new positions sans this bubble. 386 updateBubblePositions(); 387 } 388 389 @Override onChildReordered(View child, int oldIndex, int newIndex)390 void onChildReordered(View child, int oldIndex, int newIndex) { 391 updateBubblePositions(); 392 } 393 updateBubblePositions()394 private void updateBubblePositions() { 395 if (mAnimatingExpand || mAnimatingCollapse) { 396 return; 397 } 398 399 for (int i = 0; i < mLayout.getChildCount(); i++) { 400 final View bubble = mLayout.getChildAt(i); 401 402 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It 403 // will be snapped to the correct X value after the drag (if it's not dismissed). 404 if (bubble.equals(mBubbleDraggingOut)) { 405 return; 406 } 407 408 animationForChild(bubble) 409 .translationX(getBubbleLeft(i)) 410 .start(); 411 } 412 } 413 414 /** Returns the appropriate X translation value for a bubble at the given index. */ getXForChildAtIndex(int index)415 private float getXForChildAtIndex(int index) { 416 return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index; 417 } 418 419 /** 420 * @param index Bubble index in row. 421 * @return Bubble left x from left edge of screen. 422 */ getBubbleLeft(int index)423 public float getBubbleLeft(int index) { 424 float bubbleLeftFromRowLeft = index * (mBubbleSizePx + mBubblePaddingPx); 425 return getRowLeft() + bubbleLeftFromRowLeft; 426 } 427 getRowLeft()428 private float getRowLeft() { 429 if (mLayout == null) { 430 return 0; 431 } 432 int bubbleCount = mLayout.getChildCount(); 433 434 // Width calculations. 435 double bubble = bubbleCount * mBubbleSizePx; 436 float gap = (bubbleCount - 1) * mBubblePaddingPx; 437 float row = gap + (float) bubble; 438 439 float halfRow = row / 2f; 440 float centerScreen = mDisplaySize.x / 2; 441 float rowLeftFromScreenLeft = centerScreen - halfRow; 442 443 return rowLeftFromScreenLeft; 444 } 445 } 446