blob: 59d68bca93c3c895676c863d88fd8d0968f8cad7 [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
Lyn Hanf4730312019-06-18 11:18:58 -070019import android.content.res.Configuration;
Joshua Tsujif44347f2019-02-12 14:28:06 -050020import android.content.res.Resources;
Joshua Tsujidebd8312019-06-06 17:17:08 -040021import android.graphics.Path;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080022import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080023import android.graphics.PointF;
Mady Mellore19353d2019-08-21 17:25:02 -070024import android.view.DisplayCutout;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080025import android.view.View;
26import android.view.WindowInsets;
27
Joshua Tsujif49ee142019-05-29 16:32:01 -040028import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080029import androidx.dynamicanimation.animation.DynamicAnimation;
30import androidx.dynamicanimation.animation.SpringForce;
31
Joshua Tsujidebd8312019-06-06 17:17:08 -040032import com.android.systemui.Interpolators;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080033import com.android.systemui.R;
34
35import com.google.android.collect.Sets;
36
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040037import java.io.FileDescriptor;
38import java.io.PrintWriter;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080039import java.util.Set;
40
41/**
42 * Animation controller for bubbles when they're in their expanded state, or animating to/from the
43 * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
44 * dismissed.
45 */
46public class ExpandedAnimationController
47 extends PhysicsAnimationLayout.PhysicsAnimationController {
48
49 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050050 * How much to translate the bubbles when they're animating in/out. This value is multiplied by
51 * the bubble size.
52 */
53 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
54
Joshua Tsujidebd8312019-06-06 17:17:08 -040055 /** Duration of the expand/collapse target path animation. */
56 private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
Joshua Tsuji442b6272019-02-08 13:23:43 -050057
Joshua Tsujidebd8312019-06-06 17:17:08 -040058 /** Stiffness for the expand/collapse path-following animation. */
59 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080060
Mady Mellore19353d2019-08-21 17:25:02 -070061 /** What percentage of the screen to use when centering the bubbles in landscape. */
62 private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
63
Joshua Tsujib1a796b2019-01-16 15:43:12 -080064 /** Horizontal offset between bubbles, which we need to know to re-stack them. */
65 private float mStackOffsetPx;
Lyn Han4a8efe32019-05-30 09:43:27 -070066 /** Space between status bar and bubbles in the expanded state. */
67 private float mBubblePaddingTop;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080068 /** Size of each bubble. */
69 private float mBubbleSizePx;
Joshua Tsujif44347f2019-02-12 14:28:06 -050070 /** Height of the status bar. */
71 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080072 /** Size of display. */
73 private Point mDisplaySize;
Lyn Han522e9ff2019-05-17 13:26:13 -070074 /** Max number of bubbles shown in row above expanded view.*/
75 private int mBubblesMaxRendered;
Mady Mellore19353d2019-08-21 17:25:02 -070076 /** What the current screen orientation is. */
77 private int mScreenOrientation;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080078
Joshua Tsuji4accf5982019-04-22 17:36:11 -040079 /** Whether the dragged-out bubble is in the dismiss target. */
80 private boolean mIndividualBubbleWithinDismissTarget = false;
81
Joshua Tsujif49ee142019-05-29 16:32:01 -040082 private boolean mAnimatingExpand = false;
83 private boolean mAnimatingCollapse = false;
84 private Runnable mAfterExpand;
85 private Runnable mAfterCollapse;
86 private PointF mCollapsePoint;
87
Joshua Tsuji4accf5982019-04-22 17:36:11 -040088 /**
89 * Whether the dragged out bubble is springing towards the touch point, rather than using the
90 * default behavior of moving directly to the touch point.
91 *
92 * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
93 * the center. Since the touch point differs from the bubble location, we need to animate the
94 * bubble back to the touch point to avoid a jarring instant location change from the center of
95 * the target to the touch point just outside the target bounds.
96 */
97 private boolean mSpringingBubbleToTouch = false;
98
Lyn Han6f6b3ae2019-05-16 14:17:30 -070099 private int mExpandedViewPadding;
Lyn Han1b4f25e2019-06-11 13:56:34 -0700100 private float mLauncherGridDiff;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700101
Lyn Hanf4730312019-06-18 11:18:58 -0700102 public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
103 int orientation) {
Mady Mellore19353d2019-08-21 17:25:02 -0700104 updateOrientation(orientation, displaySize);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700105 mExpandedViewPadding = expandedViewPadding;
Lyn Han1b4f25e2019-06-11 13:56:34 -0700106 mLauncherGridDiff = 30f;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800107 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800108
Joshua Tsuji442b6272019-02-08 13:23:43 -0500109 /**
110 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
111 * the rest of the bubbles to animate to fill the gap.
112 */
113 private boolean mBubbleDraggedOutEnough = false;
114
115 /** The bubble currently being dragged out of the row (to potentially be dismissed). */
116 private View mBubbleDraggingOut;
117
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800118 /**
119 * Animates expanding the bubbles into a row along the top of the screen.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800120 */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400121 public void expandFromStack(Runnable after) {
122 mAnimatingCollapse = false;
123 mAnimatingExpand = true;
124 mAfterExpand = after;
Joshua Tsujic1108432019-02-22 16:10:12 -0500125
Joshua Tsujidebd8312019-06-06 17:17:08 -0400126 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800127 }
128
129 /** Animate collapsing the bubbles back to their stacked position. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400130 public void collapseBackToStack(PointF collapsePoint, Runnable after) {
131 mAnimatingExpand = false;
132 mAnimatingCollapse = true;
133 mAfterCollapse = after;
134 mCollapsePoint = collapsePoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800135
Joshua Tsujidebd8312019-06-06 17:17:08 -0400136 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400137 }
138
Lyn Hanf4730312019-06-18 11:18:58 -0700139 /**
140 * Update effective screen width based on current orientation.
141 * @param orientation Landscape or portrait.
Mady Mellore19353d2019-08-21 17:25:02 -0700142 * @param displaySize Updated display size.
Lyn Hanf4730312019-06-18 11:18:58 -0700143 */
Mady Mellore19353d2019-08-21 17:25:02 -0700144 public void updateOrientation(int orientation, Point displaySize) {
145 mScreenOrientation = orientation;
146 mDisplaySize = displaySize;
Mady Mellor818eef02019-08-16 16:12:29 -0700147 if (mLayout != null) {
148 Resources res = mLayout.getContext().getResources();
Mady Mellore19353d2019-08-21 17:25:02 -0700149 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellor818eef02019-08-16 16:12:29 -0700150 mStatusBarHeight = res.getDimensionPixelSize(
151 com.android.internal.R.dimen.status_bar_height);
Mady Mellor818eef02019-08-16 16:12:29 -0700152 }
Lyn Hanf4730312019-06-18 11:18:58 -0700153 }
154
Joshua Tsujidebd8312019-06-06 17:17:08 -0400155 /**
156 * Animates the bubbles along a curved path, either to expand them along the top or collapse
157 * them back into a stack.
158 */
159 private void startOrUpdatePathAnimation(boolean expanding) {
160 Runnable after;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400161
Joshua Tsujidebd8312019-06-06 17:17:08 -0400162 if (expanding) {
163 after = () -> {
164 mAnimatingExpand = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400165
Joshua Tsujidebd8312019-06-06 17:17:08 -0400166 if (mAfterExpand != null) {
167 mAfterExpand.run();
168 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400169
Joshua Tsujidebd8312019-06-06 17:17:08 -0400170 mAfterExpand = null;
171 };
172 } else {
173 after = () -> {
174 mAnimatingCollapse = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400175
Joshua Tsujidebd8312019-06-06 17:17:08 -0400176 if (mAfterCollapse != null) {
177 mAfterCollapse.run();
178 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400179
Joshua Tsujidebd8312019-06-06 17:17:08 -0400180 mAfterCollapse = null;
181 };
182 }
183
184 // Animate each bubble individually, since each path will end in a different spot.
185 animationsForChildrenFromIndex(0, (index, animation) -> {
186 final View bubble = mLayout.getChildAt(index);
187
188 // Start a path at the bubble's current position.
189 final Path path = new Path();
190 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
191
192 final float expandedY = getExpandedY();
193 if (expanding) {
194 // If we're expanding, first draw a line from the bubble's current position to the
195 // top of the screen.
196 path.lineTo(bubble.getTranslationX(), expandedY);
197
198 // Then, draw a line across the screen to the bubble's resting position.
199 path.lineTo(getBubbleLeft(index), expandedY);
200 } else {
201 final float sideMultiplier =
202 mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
203 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
204
205 // If we're collapsing, draw a line from the bubble's current position to the side
206 // of the screen where the bubble will be stacked.
207 path.lineTo(stackedX, expandedY);
208
209 // Then, draw a line down to the stack position.
210 path.lineTo(stackedX, mCollapsePoint.y);
211 }
212
213 // The lead bubble should be the bubble with the longest distance to travel when we're
214 // expanding, and the bubble with the shortest distance to travel when we're collapsing.
215 // During expansion from the left side, the last bubble has to travel to the far right
216 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
217 // right side, the first bubble is traveling to the top left, so it leads. During
218 // collapse to the left, the first bubble has the shortest travel time back to the stack
219 // position, so it leads (and vice versa).
220 final boolean firstBubbleLeads =
221 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
222 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
223 final int startDelay = firstBubbleLeads
224 ? (index * 10)
225 : ((mLayout.getChildCount() - index) * 10);
226
227 animation
228 .followAnimatedTargetAlongPath(
229 path,
230 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
231 Interpolators.LINEAR /* targetAnimInterpolator */)
232 .withStartDelay(startDelay)
233 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
234 }).startAll(after);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800235 }
236
Joshua Tsuji442b6272019-02-08 13:23:43 -0500237 /** Prepares the given bubble to be dragged out. */
238 public void prepareForBubbleDrag(View bubble) {
239 mLayout.cancelAnimationsOnView(bubble);
240
241 mBubbleDraggingOut = bubble;
242 mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
243 }
244
245 /**
246 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
247 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
248 * bubble is dragged back into the row.
249 */
250 public void dragBubbleOut(View bubbleView, float x, float y) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400251 if (mSpringingBubbleToTouch) {
252 if (mLayout.arePropertiesAnimatingOnView(
253 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
254 animationForChild(mBubbleDraggingOut)
255 .translationX(x)
256 .translationY(y)
257 .withStiffness(SpringForce.STIFFNESS_HIGH)
258 .start();
259 } else {
260 mSpringingBubbleToTouch = false;
261 }
262 }
263
264 if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
265 bubbleView.setTranslationX(x);
266 bubbleView.setTranslationY(y);
267 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500268
269 final boolean draggedOutEnough =
270 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
271 if (draggedOutEnough != mBubbleDraggedOutEnough) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700272 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500273 mBubbleDraggedOutEnough = draggedOutEnough;
274 }
275 }
276
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400277 /** Plays a dismiss animation on the dragged out bubble. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400278 public void dismissDraggedOutBubble(View bubble, Runnable after) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400279 mIndividualBubbleWithinDismissTarget = false;
280
Joshua Tsujif49ee142019-05-29 16:32:01 -0400281 animationForChild(bubble)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400282 .withStiffness(SpringForce.STIFFNESS_HIGH)
283 .scaleX(1.1f)
284 .scaleY(1.1f)
285 .alpha(0f, after)
286 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700287
288 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400289 }
290
Joshua Tsujif49ee142019-05-29 16:32:01 -0400291 @Nullable public View getDraggedOutBubble() {
292 return mBubbleDraggingOut;
293 }
294
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400295 /** Magnets the given bubble to the dismiss target. */
296 public void magnetBubbleToDismiss(
297 View bubbleView, float velX, float velY, float destY, Runnable after) {
298 mIndividualBubbleWithinDismissTarget = true;
299 mSpringingBubbleToTouch = false;
300 animationForChild(bubbleView)
301 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
302 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
303 .withPositionStartVelocities(velX, velY)
304 .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
305 .translationY(destY, after)
306 .start();
307 }
308
309 /**
310 * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
311 * events update the spring's final position until it's settled.
312 */
313 public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
314 mIndividualBubbleWithinDismissTarget = false;
315 mSpringingBubbleToTouch = true;
316
317 animationForChild(mBubbleDraggingOut)
318 .translationX(x)
319 .translationY(y)
320 .withPositionStartVelocities(velX, velY)
321 .withStiffness(SpringForce.STIFFNESS_HIGH)
322 .start();
323 }
324
Joshua Tsuji442b6272019-02-08 13:23:43 -0500325 /**
326 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
327 * bubbles to accommodate it if it was previously dragged out past the threshold.
328 */
329 public void snapBubbleBack(View bubbleView, float velX, float velY) {
330 final int index = mLayout.indexOfChild(bubbleView);
331
Joshua Tsujic1108432019-02-22 16:10:12 -0500332 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400333 .position(getBubbleLeft(index), getExpandedY())
334 .withPositionStartVelocities(velX, velY)
335 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500336
Lyn Han522e9ff2019-05-17 13:26:13 -0700337 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500338 }
339
Joshua Tsujif49ee142019-05-29 16:32:01 -0400340 /** Resets bubble drag out gesture flags. */
341 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500342 mBubbleDraggedOutEnough = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400343 mBubbleDraggingOut = null;
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400344 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500345 }
346
347 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800348 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
349 */
350 public void updateYPosition(Runnable after) {
351 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500352 animationsForChildrenFromIndex(
353 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800354 }
355
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800356 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800357 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800358 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
359 return 0;
360 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800361 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han4a8efe32019-05-30 09:43:27 -0700362 return mBubblePaddingTop + Math.max(
Lyn Han5aa27e22019-05-15 10:55:07 -0700363 mStatusBarHeight,
364 insets.getDisplayCutout() != null
365 ? insets.getDisplayCutout().getSafeInsetTop()
366 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800367 }
368
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400369 /** Description of current animation controller state. */
370 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
371 pw.println("ExpandedAnimationController state:");
372 pw.print(" isActive: "); pw.println(isActiveController());
373 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
374 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
375 pw.print(" bubbleInDismiss: "); pw.println(mIndividualBubbleWithinDismissTarget);
376 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
377 }
378
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800379 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400380 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
381 final Resources res = layout.getResources();
382 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400383 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400384 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
385 mStatusBarHeight =
386 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400387 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400388
389 // Ensure that all child views are at 1x scale, and visible, in case they were animating
390 // in.
391 mLayout.setVisibility(View.VISIBLE);
392 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
393 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
394 }
395
396 @Override
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800397 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
398 return Sets.newHashSet(
399 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500400 DynamicAnimation.TRANSLATION_Y,
401 DynamicAnimation.SCALE_X,
402 DynamicAnimation.SCALE_Y,
403 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800404 }
405
406 @Override
407 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
408 return NONE;
409 }
410
411 @Override
412 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
413 return 0;
414 }
415
416 @Override
417 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
418 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500419 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
420 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800421 }
422
423 @Override
424 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400425 // If a bubble is added while the expand/collapse animations are playing, update the
426 // animation to include the new bubble.
427 if (mAnimatingExpand) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400428 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400429 } else if (mAnimatingCollapse) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400430 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400431 } else {
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400432 child.setTranslationX(getBubbleLeft(index));
Joshua Tsujif49ee142019-05-29 16:32:01 -0400433 animationForChild(child)
434 .translationY(
435 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
436 getExpandedY() /* to */)
437 .start();
438 updateBubblePositions();
439 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800440 }
441
442 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500443 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500444 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500445
Joshua Tsuji442b6272019-02-08 13:23:43 -0500446 // If we're removing the dragged-out bubble, that means it got dismissed.
447 if (child.equals(mBubbleDraggingOut)) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500448 mBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400449 finishRemoval.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500450 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400451 animator.alpha(0f, finishRemoval /* endAction */)
452 .withStiffness(SpringForce.STIFFNESS_HIGH)
453 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
454 .scaleX(1.1f)
455 .scaleY(1.1f)
456 .start();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500457 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500458
459 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700460 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500461 }
462
463 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400464 void onChildReordered(View child, int oldIndex, int newIndex) {
465 updateBubblePositions();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400466
467 // We expect reordering during collapse, since we'll put the last selected bubble on top.
468 // Update the collapse animation so they end up in the right stacked positions.
469 if (mAnimatingCollapse) {
470 startOrUpdatePathAnimation(false /* expanding */);
471 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500472 }
473
Lyn Han522e9ff2019-05-17 13:26:13 -0700474 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400475 if (mAnimatingExpand || mAnimatingCollapse) {
476 return;
477 }
478
Lyn Han522e9ff2019-05-17 13:26:13 -0700479 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500480 final View bubble = mLayout.getChildAt(i);
481
482 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
483 // will be snapped to the correct X value after the drag (if it's not dismissed).
Lyn Han522e9ff2019-05-17 13:26:13 -0700484 if (bubble.equals(mBubbleDraggingOut)) {
485 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500486 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400487
Lyn Han522e9ff2019-05-17 13:26:13 -0700488 animationForChild(bubble)
489 .translationX(getBubbleLeft(i))
490 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500491 }
492 }
493
Lyn Han522e9ff2019-05-17 13:26:13 -0700494 /**
495 * @param index Bubble index in row.
496 * @return Bubble left x from left edge of screen.
497 */
498 public float getBubbleLeft(int index) {
Lyn Han5390ce32019-05-31 07:25:50 -0700499 final float bubbleFromRowLeft = index * (mBubbleSizePx + getSpaceBetweenBubbles());
Lyn Han4a8efe32019-05-30 09:43:27 -0700500 return getRowLeft() + bubbleFromRowLeft;
Lyn Han522e9ff2019-05-17 13:26:13 -0700501 }
502
Mady Mellore19353d2019-08-21 17:25:02 -0700503 /**
504 * When expanded, the bubbles are centered in the screen. In portrait, all available space is
505 * used. In landscape we have too much space so the value is restricted. This method accounts
506 * for window decorations (nav bar, cutouts).
507 *
508 * @return the desired width to display the expanded bubbles in.
509 */
510 private float getWidthForDisplayingBubbles() {
511 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
512 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
513 // display size y in landscape will be the smaller dimension of the screen
514 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
515 } else {
516 return availableWidth;
517 }
518 }
519
520 /**
521 * Determines the available screen width without the cutout.
522 *
523 * @param subtractStableInsets Whether or not stable insets should also be removed from the
524 * returned width.
525 * @return the total screen width available accounting for cutouts and insets,
526 * iff {@param includeStableInsets} is true.
527 */
528 private float getAvailableScreenWidth(boolean subtractStableInsets) {
529 float availableSize = mDisplaySize.x;
530 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
531 if (insets != null) {
532 int cutoutLeft = 0;
533 int cutoutRight = 0;
534 DisplayCutout cutout = insets.getDisplayCutout();
535 if (cutout != null) {
536 cutoutLeft = cutout.getSafeInsetLeft();
537 cutoutRight = cutout.getSafeInsetRight();
538 }
539 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
540 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
541 availableSize -= Math.max(stableLeft, cutoutLeft);
542 availableSize -= Math.max(stableRight, cutoutRight);
543 }
544 return availableSize;
545 }
546
Lyn Han522e9ff2019-05-17 13:26:13 -0700547 private float getRowLeft() {
548 if (mLayout == null) {
549 return 0;
550 }
Lyn Han4a8efe32019-05-30 09:43:27 -0700551
Lyn Han522e9ff2019-05-17 13:26:13 -0700552 int bubbleCount = mLayout.getChildCount();
Lyn Han522e9ff2019-05-17 13:26:13 -0700553
Lyn Han4a8efe32019-05-30 09:43:27 -0700554 final float totalBubbleWidth = bubbleCount * mBubbleSizePx;
555 final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
556 final float rowWidth = totalGapWidth + totalBubbleWidth;
Lyn Han522e9ff2019-05-17 13:26:13 -0700557
Mady Mellore19353d2019-08-21 17:25:02 -0700558 // This display size we're using includes the size of the insets, we want the true
559 // center of the display minus the notch here, which means we should include the
560 // stable insets (e.g. status bar, nav bar) in this calculation.
561 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
Lyn Han4a8efe32019-05-30 09:43:27 -0700562 final float halfRow = rowWidth / 2f;
Mady Mellore19353d2019-08-21 17:25:02 -0700563 final float rowLeft = trueCenter - halfRow;
Lyn Han4a8efe32019-05-30 09:43:27 -0700564
565 return rowLeft;
566 }
567
568 /**
569 * @return Space between bubbles in row above expanded view.
570 */
571 private float getSpaceBetweenBubbles() {
572 /**
573 * Ordered left to right:
574 * Screen edge
575 * [mExpandedViewPadding]
576 * Expanded view edge
577 * [launcherGridDiff] --- arbitrary value until launcher exports widths
578 * Launcher's app icon grid edge that we must match
579 */
Lyn Han1b4f25e2019-06-11 13:56:34 -0700580 final float rowMargins = (mExpandedViewPadding + mLauncherGridDiff) * 2;
Mady Mellore19353d2019-08-21 17:25:02 -0700581 final float maxRowWidth = getWidthForDisplayingBubbles() - rowMargins;
Lyn Han4a8efe32019-05-30 09:43:27 -0700582
583 final float totalBubbleWidth = mBubblesMaxRendered * mBubbleSizePx;
584 final float totalGapWidth = maxRowWidth - totalBubbleWidth;
585
586 final int gapCount = mBubblesMaxRendered - 1;
587 final float gapWidth = totalGapWidth / gapCount;
Lyn Han4a8efe32019-05-30 09:43:27 -0700588 return gapWidth;
Lyn Han522e9ff2019-05-17 13:26:13 -0700589 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800590}