blob: 1fa0e12452e12dffc09110a8e33c4efb4a96d8f4 [file] [log] [blame]
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001/*
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
17package com.android.systemui.bubbles.animation;
18
Joshua Tsujif44347f2019-02-12 14:28:06 -050019import android.content.res.Resources;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080020import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080021import android.graphics.PointF;
22import android.view.View;
23import android.view.WindowInsets;
24
Joshua Tsujif49ee142019-05-29 16:32:01 -040025import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080026import androidx.dynamicanimation.animation.DynamicAnimation;
27import androidx.dynamicanimation.animation.SpringForce;
28
29import com.android.systemui.R;
30
31import com.google.android.collect.Sets;
32
33import 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 */
40public class ExpandedAnimationController
41 extends PhysicsAnimationLayout.PhysicsAnimationController {
42
43 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050044 * 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 Tsuji442b6272019-02-08 13:23:43 -050049 /** How much to scale down bubbles when they're animating in/out. */
50 private static final float ANIMATE_SCALE_PERCENT = 0.5f;
51
Joshua Tsuji3829caa2019-03-05 18:09:13 -050052 /** The stack position to collapse back to in {@link #collapseBackToStack}. */
53 private PointF mCollapseToPoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080054
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 Tsujif44347f2019-02-12 14:28:06 -050061 /** Height of the status bar. */
62 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080063 /** Size of display. */
64 private Point mDisplaySize;
65 /** Size of dismiss target at bottom of screen. */
66 private float mPipDismissHeight;
67
Joshua Tsuji4accf5982019-04-22 17:36:11 -040068 /** Whether the dragged-out bubble is in the dismiss target. */
69 private boolean mIndividualBubbleWithinDismissTarget = false;
70
Joshua Tsujif49ee142019-05-29 16:32:01 -040071 private boolean mAnimatingExpand = false;
72 private boolean mAnimatingCollapse = false;
73 private Runnable mAfterExpand;
74 private Runnable mAfterCollapse;
75 private PointF mCollapsePoint;
76
Joshua Tsuji4accf5982019-04-22 17:36:11 -040077 /**
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
Lyn Han6f6b3ae2019-05-16 14:17:30 -070088 private int mExpandedViewPadding;
89
90 public ExpandedAnimationController(Point displaySize, int expandedViewPadding) {
Mady Mellor44ee2fe2019-01-30 17:51:16 -080091 mDisplaySize = displaySize;
Lyn Han6f6b3ae2019-05-16 14:17:30 -070092 mExpandedViewPadding = expandedViewPadding;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080093 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -080094
Joshua Tsuji442b6272019-02-08 13:23:43 -050095 /**
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 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800105 * Animates expanding the bubbles into a row along the top of the screen.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800106 */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400107 public void expandFromStack(Runnable after) {
108 mAnimatingCollapse = false;
109 mAnimatingExpand = true;
110 mAfterExpand = after;
Joshua Tsujic1108432019-02-22 16:10:12 -0500111
Joshua Tsujif49ee142019-05-29 16:32:01 -0400112 startOrUpdateExpandAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800113 }
114
115 /** Animate collapsing the bubbles back to their stacked position. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400116 public void collapseBackToStack(PointF collapsePoint, Runnable after) {
117 mAnimatingExpand = false;
118 mAnimatingCollapse = true;
119 mAfterCollapse = after;
120 mCollapsePoint = collapsePoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800121
Joshua Tsujif49ee142019-05-29 16:32:01 -0400122 startOrUpdateCollapseAnimation();
123 }
124
125 private void startOrUpdateExpandAnimation() {
Joshua Tsujic1108432019-02-22 16:10:12 -0500126 animationsForChildrenFromIndex(
127 0, /* startIndex */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400128 (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
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) -> {
Joshua Tsujic1108432019-02-22 16:10:12 -0500146 animation.position(
Joshua Tsujif49ee142019-05-29 16:32:01 -0400147 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 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800159 }
160
Joshua Tsuji442b6272019-02-08 13:23:43 -0500161 /** Prepares the given bubble to be dragged out. */
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 */
174 public void dragBubbleOut(View bubbleView, float x, float y) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400175 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 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500192
193 final boolean draggedOutEnough =
194 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
195 if (draggedOutEnough != mBubbleDraggedOutEnough) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700196 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500197 mBubbleDraggedOutEnough = draggedOutEnough;
198 }
199 }
200
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400201 /** Plays a dismiss animation on the dragged out bubble. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400202 public void dismissDraggedOutBubble(View bubble, Runnable after) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400203 mIndividualBubbleWithinDismissTarget = false;
204
Joshua Tsujif49ee142019-05-29 16:32:01 -0400205 animationForChild(bubble)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400206 .withStiffness(SpringForce.STIFFNESS_HIGH)
207 .scaleX(1.1f)
208 .scaleY(1.1f)
209 .alpha(0f, after)
210 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700211
212 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400213 }
214
Joshua Tsujif49ee142019-05-29 16:32:01 -0400215 @Nullable public View getDraggedOutBubble() {
216 return mBubbleDraggingOut;
217 }
218
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400219 /** 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
Joshua Tsuji442b6272019-02-08 13:23:43 -0500249 /**
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
Joshua Tsujic1108432019-02-22 16:10:12 -0500256 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400257 .position(getBubbleLeft(index), getExpandedY())
258 .withPositionStartVelocities(velX, velY)
259 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500260
Lyn Han522e9ff2019-05-17 13:26:13 -0700261 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500262 }
263
Joshua Tsujif49ee142019-05-29 16:32:01 -0400264 /** Resets bubble drag out gesture flags. */
265 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500266 mBubbleDraggedOutEnough = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400267 mBubbleDraggingOut = null;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500268 }
269
270 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800271 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
272 */
273 public void updateYPosition(Runnable after) {
274 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500275 animationsForChildrenFromIndex(
276 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800277 }
278
279 /**
Joshua Tsuji442b6272019-02-08 13:23:43 -0500280 * 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 */
284 private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500285 animationsForChildrenFromIndex(
286 startIndex,
287 (index, animation) ->
288 animation.translationX(getXForChildAtIndex(index + numBubbleWidths)))
289 .startAll();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500290 }
291
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800292 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800293 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800294 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
295 return 0;
296 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800297 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han5aa27e22019-05-15 10:55:07 -0700298 return mBubblePaddingPx + Math.max(
299 mStatusBarHeight,
300 insets.getDisplayCutout() != null
301 ? insets.getDisplayCutout().getSafeInsetTop()
302 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800303 }
304
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800305 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400306 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);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400314
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
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800323 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
324 return Sets.newHashSet(
325 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500326 DynamicAnimation.TRANSLATION_Y,
327 DynamicAnimation.SCALE_X,
328 DynamicAnimation.SCALE_Y,
329 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800330 }
331
332 @Override
333 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
334 return NONE;
335 }
336
337 @Override
338 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
339 return 0;
340 }
341
342 @Override
343 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
344 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500345 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
346 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800347 }
348
349 @Override
350 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400351 // 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 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800366 }
367
368 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500369 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500370 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500371
Joshua Tsuji442b6272019-02-08 13:23:43 -0500372 // If we're removing the dragged-out bubble, that means it got dismissed.
373 if (child.equals(mBubbleDraggingOut)) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500374 mBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400375 finishRemoval.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500376 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400377 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();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500383 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500384
385 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700386 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500387 }
388
Joshua Tsujif49ee142019-05-29 16:32:01 -0400389 @Override
390 void onChildReordered(View child, int oldIndex, int newIndex) {
391 updateBubblePositions();
392 }
393
Lyn Han522e9ff2019-05-17 13:26:13 -0700394 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400395 if (mAnimatingExpand || mAnimatingCollapse) {
396 return;
397 }
398
Lyn Han522e9ff2019-05-17 13:26:13 -0700399 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500400 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).
Lyn Han522e9ff2019-05-17 13:26:13 -0700404 if (bubble.equals(mBubbleDraggingOut)) {
405 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500406 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400407
Lyn Han522e9ff2019-05-17 13:26:13 -0700408 animationForChild(bubble)
409 .translationX(getBubbleLeft(i))
410 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500411 }
412 }
413
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500414 /** Returns the appropriate X translation value for a bubble at the given index. */
415 private float getXForChildAtIndex(int index) {
416 return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800417 }
Lyn Han522e9ff2019-05-17 13:26:13 -0700418
419 /**
420 * @param index Bubble index in row.
421 * @return Bubble left x from left edge of screen.
422 */
423 public float getBubbleLeft(int index) {
424 float bubbleLeftFromRowLeft = index * (mBubbleSizePx + mBubblePaddingPx);
425 return getRowLeft() + bubbleLeftFromRowLeft;
426 }
427
428 private float getRowLeft() {
429 if (mLayout == null) {
430 return 0;
431 }
432 int bubbleCount = mLayout.getChildCount();
Joshua Tsujicb97a112019-05-29 16:20:41 -0400433
Lyn Han522e9ff2019-05-17 13:26:13 -0700434 // 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 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800445}