Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | |
Joshua Tsuji | f44347f | 2019-02-12 14:28:06 -0500 | [diff] [blame] | 19 | import android.content.res.Resources; |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 20 | import android.graphics.Point; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 21 | import android.graphics.PointF; |
| 22 | import android.view.View; |
| 23 | import android.view.WindowInsets; |
| 24 | |
| 25 | import androidx.dynamicanimation.animation.DynamicAnimation; |
| 26 | import androidx.dynamicanimation.animation.SpringForce; |
| 27 | |
| 28 | import com.android.systemui.R; |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 29 | import com.android.systemui.bubbles.BubbleController; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | /** |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 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 | |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 49 | /** How much to scale down bubbles when they're animating in/out. */ |
| 50 | private static final float ANIMATE_SCALE_PERCENT = 0.5f; |
| 51 | |
Joshua Tsuji | 3829caa | 2019-03-05 18:09:13 -0500 | [diff] [blame] | 52 | /** The stack position to collapse back to in {@link #collapseBackToStack}. */ |
| 53 | private PointF mCollapseToPoint; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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; |
Joshua Tsuji | f44347f | 2019-02-12 14:28:06 -0500 | [diff] [blame] | 61 | /** Height of the status bar. */ |
| 62 | private float mStatusBarHeight; |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 63 | /** Size of display. */ |
| 64 | private Point mDisplaySize; |
| 65 | /** Size of dismiss target at bottom of screen. */ |
| 66 | private float mPipDismissHeight; |
| 67 | |
| 68 | public ExpandedAnimationController(Point displaySize) { |
| 69 | mDisplaySize = displaySize; |
| 70 | } |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 71 | |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 72 | /** |
| 73 | * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause |
| 74 | * the rest of the bubbles to animate to fill the gap. |
| 75 | */ |
| 76 | private boolean mBubbleDraggedOutEnough = false; |
| 77 | |
| 78 | /** The bubble currently being dragged out of the row (to potentially be dismissed). */ |
| 79 | private View mBubbleDraggingOut; |
| 80 | |
| 81 | /** |
| 82 | * Drag velocities for the dragging-out bubble when the drag finished. These are used by |
| 83 | * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity. |
| 84 | */ |
| 85 | private float mBubbleDraggingOutVelX; |
| 86 | private float mBubbleDraggingOutVelY; |
| 87 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 88 | @Override |
| 89 | protected void setLayout(PhysicsAnimationLayout layout) { |
| 90 | super.setLayout(layout); |
Joshua Tsuji | f44347f | 2019-02-12 14:28:06 -0500 | [diff] [blame] | 91 | |
| 92 | final Resources res = layout.getResources(); |
| 93 | mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); |
| 94 | mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding); |
| 95 | mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); |
| 96 | mStatusBarHeight = |
| 97 | res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 98 | mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Animates expanding the bubbles into a row along the top of the screen. |
| 103 | * |
| 104 | * @return The y-value to which the bubbles were expanded, in case that's useful. |
| 105 | */ |
Joshua Tsuji | 3829caa | 2019-03-05 18:09:13 -0500 | [diff] [blame] | 106 | public float expandFromStack(PointF collapseTo, Runnable after) { |
| 107 | mCollapseToPoint = collapseTo; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 108 | |
| 109 | // How much to translate the next bubble, so that it is not overlapping the previous one. |
| 110 | float translateNextBubbleXBy = mBubblePaddingPx; |
| 111 | for (int i = 0; i < mLayout.getChildCount(); i++) { |
| 112 | mLayout.animatePositionForChildAtIndex(i, translateNextBubbleXBy, getExpandedY()); |
| 113 | translateNextBubbleXBy += mBubbleSizePx + mBubblePaddingPx; |
| 114 | } |
| 115 | |
| 116 | runAfterTranslationsEnd(after); |
| 117 | return getExpandedY(); |
| 118 | } |
| 119 | |
| 120 | /** Animate collapsing the bubbles back to their stacked position. */ |
| 121 | public void collapseBackToStack(Runnable after) { |
| 122 | // Stack to the left if we're going to the left, or right if not. |
Joshua Tsuji | 3829caa | 2019-03-05 18:09:13 -0500 | [diff] [blame] | 123 | final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 124 | for (int i = 0; i < mLayout.getChildCount(); i++) { |
| 125 | mLayout.animatePositionForChildAtIndex( |
Joshua Tsuji | 3829caa | 2019-03-05 18:09:13 -0500 | [diff] [blame] | 126 | i, |
| 127 | mCollapseToPoint.x + (sideMultiplier * i * mStackOffsetPx), mCollapseToPoint.y); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | runAfterTranslationsEnd(after); |
| 131 | } |
| 132 | |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 133 | /** Prepares the given bubble to be dragged out. */ |
| 134 | public void prepareForBubbleDrag(View bubble) { |
| 135 | mLayout.cancelAnimationsOnView(bubble); |
| 136 | |
| 137 | mBubbleDraggingOut = bubble; |
| 138 | mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE); |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to |
| 143 | * take its place once it's dragged out of the row of bubbles, and animate out of the way if the |
| 144 | * bubble is dragged back into the row. |
| 145 | */ |
| 146 | public void dragBubbleOut(View bubbleView, float x, float y) { |
| 147 | bubbleView.setTranslationX(x); |
| 148 | bubbleView.setTranslationY(y); |
| 149 | |
| 150 | final boolean draggedOutEnough = |
| 151 | y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; |
| 152 | if (draggedOutEnough != mBubbleDraggedOutEnough) { |
| 153 | animateStackByBubbleWidthsStartingFrom( |
| 154 | /* numBubbleWidths */ draggedOutEnough ? -1 : 0, |
| 155 | /* startIndex */ mLayout.indexOfChild(bubbleView) + 1); |
| 156 | mBubbleDraggedOutEnough = draggedOutEnough; |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * Snaps a bubble back to its position within the bubble row, and animates the rest of the |
| 162 | * bubbles to accommodate it if it was previously dragged out past the threshold. |
| 163 | */ |
| 164 | public void snapBubbleBack(View bubbleView, float velX, float velY) { |
| 165 | final int index = mLayout.indexOfChild(bubbleView); |
| 166 | |
| 167 | // Snap the bubble back, respecting its current velocity. |
| 168 | mLayout.animateValueForChildAtIndex( |
| 169 | DynamicAnimation.TRANSLATION_X, index, getXForChildAtIndex(index), velX); |
| 170 | mLayout.animateValueForChildAtIndex( |
| 171 | DynamicAnimation.TRANSLATION_Y, index, getExpandedY(), velY); |
| 172 | mLayout.setEndListenerForProperties( |
| 173 | mLayout.new OneTimeMultiplePropertyEndListener() { |
| 174 | @Override |
| 175 | void onAllAnimationsForPropertiesEnd() { |
| 176 | // Reset Z translation once the bubble is done snapping back. |
| 177 | bubbleView.setTranslationZ(0f); |
| 178 | } |
| 179 | }, |
| 180 | DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); |
| 181 | |
| 182 | animateStackByBubbleWidthsStartingFrom( |
| 183 | /* numBubbleWidths */ 0, /* startIndex */ index + 1); |
| 184 | |
| 185 | mBubbleDraggingOut = null; |
| 186 | mBubbleDraggedOutEnough = false; |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Sets configuration variables so that when the given bubble is removed, the animations are |
| 191 | * started with the given velocities. |
| 192 | */ |
| 193 | public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) { |
| 194 | mBubbleDraggingOut = bubbleView; |
| 195 | mBubbleDraggingOutVelX = velX; |
| 196 | mBubbleDraggingOutVelY = velY; |
| 197 | mBubbleDraggedOutEnough = false; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Animates the bubbles, starting at the given index, to the left or right by the given number |
| 202 | * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal |
| 203 | * positions. |
| 204 | */ |
| 205 | private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) { |
| 206 | for (int i = startIndex; i < mLayout.getChildCount(); i++) { |
| 207 | mLayout.animateValueForChildAtIndex( |
| 208 | DynamicAnimation.TRANSLATION_X, |
| 209 | i, |
| 210 | getXForChildAtIndex(i + numBubbleWidths)); |
| 211 | } |
| 212 | } |
| 213 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 214 | /** The Y value of the row of expanded bubbles. */ |
Mady Mellor | fe7ec03 | 2019-01-30 17:32:49 -0800 | [diff] [blame] | 215 | public float getExpandedY() { |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 216 | boolean showOnTop = mLayout != null |
| 217 | && BubbleController.showBubblesAtTop(mLayout.getContext()); |
Mady Mellor | fe7ec03 | 2019-01-30 17:32:49 -0800 | [diff] [blame] | 218 | final WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null; |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 219 | if (showOnTop && insets != null) { |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 220 | return mBubblePaddingPx + Math.max( |
Joshua Tsuji | f44347f | 2019-02-12 14:28:06 -0500 | [diff] [blame] | 221 | mStatusBarHeight, |
Joshua Tsuji | 0fee768 | 2019-01-25 11:37:49 -0500 | [diff] [blame] | 222 | insets.getDisplayCutout() != null |
| 223 | ? insets.getDisplayCutout().getSafeInsetTop() |
| 224 | : 0); |
Mady Mellor | 44ee2fe | 2019-01-30 17:51:16 -0800 | [diff] [blame] | 225 | } else { |
| 226 | int bottomInset = insets != null ? insets.getSystemWindowInsetBottom() : 0; |
| 227 | return mDisplaySize.y - mBubbleSizePx - (mPipDismissHeight - bottomInset); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 228 | } |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 229 | } |
| 230 | |
| 231 | /** Runs the given Runnable after all translation-related animations have ended. */ |
| 232 | private void runAfterTranslationsEnd(Runnable after) { |
| 233 | DynamicAnimation.OnAnimationEndListener allEndedListener = |
| 234 | (animation, canceled, value, velocity) -> { |
| 235 | if (!mLayout.arePropertiesAnimating( |
| 236 | DynamicAnimation.TRANSLATION_X, |
| 237 | DynamicAnimation.TRANSLATION_Y)) { |
| 238 | after.run(); |
| 239 | } |
| 240 | }; |
| 241 | |
| 242 | mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_X); |
| 243 | mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_Y); |
| 244 | } |
| 245 | |
| 246 | @Override |
| 247 | Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { |
| 248 | return Sets.newHashSet( |
| 249 | DynamicAnimation.TRANSLATION_X, |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 250 | DynamicAnimation.TRANSLATION_Y, |
| 251 | DynamicAnimation.SCALE_X, |
| 252 | DynamicAnimation.SCALE_Y, |
| 253 | DynamicAnimation.ALPHA); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 254 | } |
| 255 | |
| 256 | @Override |
| 257 | int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { |
| 258 | return NONE; |
| 259 | } |
| 260 | |
| 261 | @Override |
| 262 | float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { |
| 263 | return 0; |
| 264 | } |
| 265 | |
| 266 | @Override |
| 267 | SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { |
| 268 | return new SpringForce() |
Joshua Tsuji | 010c2b1 | 2019-02-25 18:11:25 -0500 | [diff] [blame] | 269 | .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) |
| 270 | .setStiffness(SpringForce.STIFFNESS_LOW); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 271 | } |
| 272 | |
| 273 | @Override |
| 274 | void onChildAdded(View child, int index) { |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 275 | // Pop in from the top. |
| 276 | // TODO: Reverse this when bubbles are at the bottom. |
| 277 | child.setTranslationX(getXForChildAtIndex(index)); |
| 278 | child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR); |
| 279 | mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY()); |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 280 | animateBubblesAfterIndexToCorrectX(index); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 281 | } |
| 282 | |
| 283 | @Override |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 284 | void onChildRemoved(View child, int index, Runnable finishRemoval) { |
| 285 | // Bubble pops out to the top. |
| 286 | // TODO: Reverse this when bubbles are at the bottom. |
| 287 | mLayout.animateValueForChild( |
| 288 | DynamicAnimation.ALPHA, child, 0f, finishRemoval); |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 289 | |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 290 | // If we're removing the dragged-out bubble, that means it got dismissed. |
| 291 | if (child.equals(mBubbleDraggingOut)) { |
| 292 | // Throw it to the bottom of the screen, towards the center horizontally. |
| 293 | mLayout.animateValueForChild( |
| 294 | DynamicAnimation.TRANSLATION_X, |
| 295 | child, |
| 296 | mLayout.getWidth() / 2f - mBubbleSizePx / 2f, |
| 297 | mBubbleDraggingOutVelX); |
| 298 | mLayout.animateValueForChild( |
| 299 | DynamicAnimation.TRANSLATION_Y, |
| 300 | child, |
| 301 | mLayout.getHeight() + mBubbleSizePx, |
| 302 | mBubbleDraggingOutVelY); |
| 303 | |
| 304 | // Scale it down a bit so it looks like it's disappearing. |
| 305 | mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_SCALE_PERCENT); |
| 306 | mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_SCALE_PERCENT); |
| 307 | |
| 308 | mBubbleDraggingOut = null; |
| 309 | } else { |
| 310 | // If we're removing some random bubble just throw it off the top. |
| 311 | mLayout.animateValueForChild( |
| 312 | DynamicAnimation.TRANSLATION_Y, |
| 313 | child, |
| 314 | getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR); |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 315 | } |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 316 | |
| 317 | // Animate all the other bubbles to their new positions sans this bubble. |
| 318 | animateBubblesAfterIndexToCorrectX(index); |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 319 | } |
| 320 | |
| 321 | @Override |
| 322 | protected void setChildVisibility(View child, int index, int visibility) { |
| 323 | if (visibility == View.VISIBLE) { |
| 324 | // Set alpha to 0 but then become visible immediately so the animation is visible. |
| 325 | child.setAlpha(0f); |
| 326 | child.setVisibility(View.VISIBLE); |
| 327 | } |
| 328 | |
| 329 | // Fade in. |
| 330 | mLayout.animateValueForChild( |
| 331 | DynamicAnimation.ALPHA, |
| 332 | child, |
| 333 | /* value */ visibility == View.GONE ? 0f : 1f, |
| 334 | () -> super.setChildVisibility(child, index, visibility)); |
| 335 | } |
| 336 | |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 337 | /** |
| 338 | * Animates the bubbles after the given index to the X position they should be in according to |
| 339 | * {@link #getXForChildAtIndex}. |
| 340 | */ |
| 341 | private void animateBubblesAfterIndexToCorrectX(int start) { |
| 342 | for (int i = start; i < mLayout.getChildCount(); i++) { |
| 343 | final View bubble = mLayout.getChildAt(i); |
| 344 | |
| 345 | // Don't animate the dragging out bubble, or it'll jump around while being dragged. It |
| 346 | // will be snapped to the correct X value after the drag (if it's not dismissed). |
| 347 | if (!bubble.equals(mBubbleDraggingOut)) { |
| 348 | mLayout.animateValueForChild( |
| 349 | DynamicAnimation.TRANSLATION_X, bubble, getXForChildAtIndex(i)); |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 354 | /** Returns the appropriate X translation value for a bubble at the given index. */ |
| 355 | private float getXForChildAtIndex(int index) { |
| 356 | return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 357 | } |
| 358 | } |