blob: d974adc34ee03e823c03251db1ac928462b81bab [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 Tsuji20103542020-02-18 14:06:28 -050028import androidx.annotation.NonNull;
Joshua Tsujif49ee142019-05-29 16:32:01 -040029import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080030import androidx.dynamicanimation.animation.DynamicAnimation;
31import androidx.dynamicanimation.animation.SpringForce;
32
Joshua Tsujidebd8312019-06-06 17:17:08 -040033import com.android.systemui.Interpolators;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080034import com.android.systemui.R;
Joshua Tsuji20103542020-02-18 14:06:28 -050035import com.android.systemui.util.magnetictarget.MagnetizedObject;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080036
37import com.google.android.collect.Sets;
38
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040039import java.io.FileDescriptor;
40import java.io.PrintWriter;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080041import java.util.Set;
42
43/**
44 * Animation controller for bubbles when they're in their expanded state, or animating to/from the
45 * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
46 * dismissed.
47 */
48public class ExpandedAnimationController
49 extends PhysicsAnimationLayout.PhysicsAnimationController {
50
51 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050052 * How much to translate the bubbles when they're animating in/out. This value is multiplied by
53 * the bubble size.
54 */
55 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
56
Joshua Tsujidebd8312019-06-06 17:17:08 -040057 /** Duration of the expand/collapse target path animation. */
58 private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
Joshua Tsuji442b6272019-02-08 13:23:43 -050059
Joshua Tsujidebd8312019-06-06 17:17:08 -040060 /** Stiffness for the expand/collapse path-following animation. */
61 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080062
Mady Mellore19353d2019-08-21 17:25:02 -070063 /** What percentage of the screen to use when centering the bubbles in landscape. */
64 private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
65
Joshua Tsuji20103542020-02-18 14:06:28 -050066 /**
67 * Velocity required to dismiss an individual bubble without dragging it into the dismiss
68 * target.
69 */
70 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
71
Joshua Tsujib1a796b2019-01-16 15:43:12 -080072 /** Horizontal offset between bubbles, which we need to know to re-stack them. */
73 private float mStackOffsetPx;
Lyn Han4a8efe32019-05-30 09:43:27 -070074 /** Space between status bar and bubbles in the expanded state. */
75 private float mBubblePaddingTop;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080076 /** Size of each bubble. */
77 private float mBubbleSizePx;
Lyn Hanc47e1712020-01-28 21:43:34 -080078 /** Space between bubbles in row above expanded view. */
79 private float mSpaceBetweenBubbles;
Joshua Tsujif44347f2019-02-12 14:28:06 -050080 /** Height of the status bar. */
81 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080082 /** Size of display. */
83 private Point mDisplaySize;
Lyn Hanc47e1712020-01-28 21:43:34 -080084 /** Max number of bubbles shown in row above expanded view. */
Lyn Han522e9ff2019-05-17 13:26:13 -070085 private int mBubblesMaxRendered;
Mady Mellore19353d2019-08-21 17:25:02 -070086 /** What the current screen orientation is. */
87 private int mScreenOrientation;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080088
Joshua Tsujif49ee142019-05-29 16:32:01 -040089 private boolean mAnimatingExpand = false;
90 private boolean mAnimatingCollapse = false;
Lyn Hanb58c7562020-01-07 14:29:20 -080091 private @Nullable Runnable mAfterExpand;
Joshua Tsujif49ee142019-05-29 16:32:01 -040092 private Runnable mAfterCollapse;
93 private PointF mCollapsePoint;
94
Joshua Tsuji4accf5982019-04-22 17:36:11 -040095 /**
96 * Whether the dragged out bubble is springing towards the touch point, rather than using the
97 * default behavior of moving directly to the touch point.
98 *
99 * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
100 * the center. Since the touch point differs from the bubble location, we need to animate the
101 * bubble back to the touch point to avoid a jarring instant location change from the center of
102 * the target to the touch point just outside the target bounds.
103 */
104 private boolean mSpringingBubbleToTouch = false;
105
Joshua Tsuji20103542020-02-18 14:06:28 -0500106 /**
107 * Whether to spring the bubble to the next touch event coordinates. This is used to animate the
108 * bubble out of the magnetic dismiss target to the touch location.
109 *
110 * Once it 'catches up' and the animation ends, we'll revert to moving it directly.
111 */
112 private boolean mSpringToTouchOnNextMotionEvent = false;
113
114 /** The bubble currently being dragged out of the row (to potentially be dismissed). */
115 private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
116
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700117 private int mExpandedViewPadding;
118
Lyn Hanf4730312019-06-18 11:18:58 -0700119 public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
120 int orientation) {
Mady Mellore19353d2019-08-21 17:25:02 -0700121 updateOrientation(orientation, displaySize);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700122 mExpandedViewPadding = expandedViewPadding;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800123 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800124
Joshua Tsuji442b6272019-02-08 13:23:43 -0500125 /**
126 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
127 * the rest of the bubbles to animate to fill the gap.
128 */
129 private boolean mBubbleDraggedOutEnough = false;
130
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800131 /**
132 * Animates expanding the bubbles into a row along the top of the screen.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800133 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800134 public void expandFromStack(@Nullable Runnable after) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400135 mAnimatingCollapse = false;
136 mAnimatingExpand = true;
137 mAfterExpand = after;
Joshua Tsujic1108432019-02-22 16:10:12 -0500138
Joshua Tsujidebd8312019-06-06 17:17:08 -0400139 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800140 }
141
142 /** Animate collapsing the bubbles back to their stacked position. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400143 public void collapseBackToStack(PointF collapsePoint, Runnable after) {
144 mAnimatingExpand = false;
145 mAnimatingCollapse = true;
146 mAfterCollapse = after;
147 mCollapsePoint = collapsePoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800148
Joshua Tsujidebd8312019-06-06 17:17:08 -0400149 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400150 }
151
Lyn Hanf4730312019-06-18 11:18:58 -0700152 /**
153 * Update effective screen width based on current orientation.
154 * @param orientation Landscape or portrait.
Mady Mellore19353d2019-08-21 17:25:02 -0700155 * @param displaySize Updated display size.
Lyn Hanf4730312019-06-18 11:18:58 -0700156 */
Mady Mellore19353d2019-08-21 17:25:02 -0700157 public void updateOrientation(int orientation, Point displaySize) {
158 mScreenOrientation = orientation;
159 mDisplaySize = displaySize;
Mady Mellor818eef02019-08-16 16:12:29 -0700160 if (mLayout != null) {
161 Resources res = mLayout.getContext().getResources();
Mady Mellore19353d2019-08-21 17:25:02 -0700162 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellor818eef02019-08-16 16:12:29 -0700163 mStatusBarHeight = res.getDimensionPixelSize(
164 com.android.internal.R.dimen.status_bar_height);
Mady Mellor818eef02019-08-16 16:12:29 -0700165 }
Lyn Hanf4730312019-06-18 11:18:58 -0700166 }
167
Joshua Tsujidebd8312019-06-06 17:17:08 -0400168 /**
169 * Animates the bubbles along a curved path, either to expand them along the top or collapse
170 * them back into a stack.
171 */
172 private void startOrUpdatePathAnimation(boolean expanding) {
173 Runnable after;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400174
Joshua Tsujidebd8312019-06-06 17:17:08 -0400175 if (expanding) {
176 after = () -> {
177 mAnimatingExpand = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400178
Joshua Tsujidebd8312019-06-06 17:17:08 -0400179 if (mAfterExpand != null) {
180 mAfterExpand.run();
181 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400182
Joshua Tsujidebd8312019-06-06 17:17:08 -0400183 mAfterExpand = null;
184 };
185 } else {
186 after = () -> {
187 mAnimatingCollapse = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400188
Joshua Tsujidebd8312019-06-06 17:17:08 -0400189 if (mAfterCollapse != null) {
190 mAfterCollapse.run();
191 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400192
Joshua Tsujidebd8312019-06-06 17:17:08 -0400193 mAfterCollapse = null;
194 };
195 }
196
197 // Animate each bubble individually, since each path will end in a different spot.
198 animationsForChildrenFromIndex(0, (index, animation) -> {
199 final View bubble = mLayout.getChildAt(index);
200
201 // Start a path at the bubble's current position.
202 final Path path = new Path();
203 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
204
205 final float expandedY = getExpandedY();
206 if (expanding) {
207 // If we're expanding, first draw a line from the bubble's current position to the
208 // top of the screen.
209 path.lineTo(bubble.getTranslationX(), expandedY);
210
211 // Then, draw a line across the screen to the bubble's resting position.
212 path.lineTo(getBubbleLeft(index), expandedY);
213 } else {
214 final float sideMultiplier =
215 mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
216 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
217
218 // If we're collapsing, draw a line from the bubble's current position to the side
219 // of the screen where the bubble will be stacked.
220 path.lineTo(stackedX, expandedY);
221
222 // Then, draw a line down to the stack position.
223 path.lineTo(stackedX, mCollapsePoint.y);
224 }
225
226 // The lead bubble should be the bubble with the longest distance to travel when we're
227 // expanding, and the bubble with the shortest distance to travel when we're collapsing.
228 // During expansion from the left side, the last bubble has to travel to the far right
229 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
230 // right side, the first bubble is traveling to the top left, so it leads. During
231 // collapse to the left, the first bubble has the shortest travel time back to the stack
232 // position, so it leads (and vice versa).
233 final boolean firstBubbleLeads =
234 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
235 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
236 final int startDelay = firstBubbleLeads
237 ? (index * 10)
238 : ((mLayout.getChildCount() - index) * 10);
239
240 animation
241 .followAnimatedTargetAlongPath(
242 path,
243 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
244 Interpolators.LINEAR /* targetAnimInterpolator */)
245 .withStartDelay(startDelay)
246 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
247 }).startAll(after);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800248 }
249
Joshua Tsuji20103542020-02-18 14:06:28 -0500250 /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
251 public void onUnstuckFromTarget() {
252 mSpringToTouchOnNextMotionEvent = true;
253 }
254
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400255 /**
256 * Prepares the given bubble view to be dragged out, using the provided magnetic target and
257 * listener.
258 */
259 public void prepareForBubbleDrag(
260 View bubble,
261 MagnetizedObject.MagneticTarget target,
262 MagnetizedObject.MagnetListener listener) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500263 mLayout.cancelAnimationsOnView(bubble);
264
Joshua Tsuji20103542020-02-18 14:06:28 -0500265 bubble.setTranslationZ(Short.MAX_VALUE);
266 mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
267 mLayout.getContext(), bubble,
268 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
269 @Override
270 public float getWidth(@NonNull View underlyingObject) {
271 return mBubbleSizePx;
272 }
273
274 @Override
275 public float getHeight(@NonNull View underlyingObject) {
276 return mBubbleSizePx;
277 }
278
279 @Override
280 public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
281 loc[0] = (int) bubble.getTranslationX();
282 loc[1] = (int) bubble.getTranslationY();
283 }
284 };
285 mMagnetizedBubbleDraggingOut.addTarget(target);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400286 mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
Joshua Tsuji20103542020-02-18 14:06:28 -0500287 mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
288 mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
289 }
290
291 private void springBubbleTo(View bubble, float x, float y) {
292 animationForChild(bubble)
293 .translationX(x)
294 .translationY(y)
295 .withStiffness(SpringForce.STIFFNESS_HIGH)
296 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500297 }
298
299 /**
300 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
301 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
302 * bubble is dragged back into the row.
303 */
304 public void dragBubbleOut(View bubbleView, float x, float y) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500305 if (mSpringToTouchOnNextMotionEvent) {
306 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
307 mSpringToTouchOnNextMotionEvent = false;
308 mSpringingBubbleToTouch = true;
309 } else if (mSpringingBubbleToTouch) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400310 if (mLayout.arePropertiesAnimatingOnView(
311 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500312 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400313 } else {
314 mSpringingBubbleToTouch = false;
315 }
316 }
317
Joshua Tsuji20103542020-02-18 14:06:28 -0500318 if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400319 bubbleView.setTranslationX(x);
320 bubbleView.setTranslationY(y);
321 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500322
323 final boolean draggedOutEnough =
324 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
325 if (draggedOutEnough != mBubbleDraggedOutEnough) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700326 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500327 mBubbleDraggedOutEnough = draggedOutEnough;
328 }
329 }
330
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400331 /** Plays a dismiss animation on the dragged out bubble. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400332 public void dismissDraggedOutBubble(View bubble, Runnable after) {
Mady Mellor12c90952020-04-06 12:29:07 -0700333 if (bubble == null) {
334 return;
335 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400336 animationForChild(bubble)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400337 .withStiffness(SpringForce.STIFFNESS_HIGH)
338 .scaleX(1.1f)
339 .scaleY(1.1f)
340 .alpha(0f, after)
341 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700342
343 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400344 }
345
Joshua Tsujif49ee142019-05-29 16:32:01 -0400346 @Nullable public View getDraggedOutBubble() {
Joshua Tsuji20103542020-02-18 14:06:28 -0500347 return mMagnetizedBubbleDraggingOut == null
348 ? null
349 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
Joshua Tsujif49ee142019-05-29 16:32:01 -0400350 }
351
Joshua Tsuji20103542020-02-18 14:06:28 -0500352 /** Returns the MagnetizedObject instance for the dragging-out bubble. */
353 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
354 return mMagnetizedBubbleDraggingOut;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400355 }
356
Joshua Tsuji442b6272019-02-08 13:23:43 -0500357 /**
358 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
359 * bubbles to accommodate it if it was previously dragged out past the threshold.
360 */
361 public void snapBubbleBack(View bubbleView, float velX, float velY) {
362 final int index = mLayout.indexOfChild(bubbleView);
363
Joshua Tsujic1108432019-02-22 16:10:12 -0500364 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400365 .position(getBubbleLeft(index), getExpandedY())
366 .withPositionStartVelocities(velX, velY)
367 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500368
Joshua Tsuji20103542020-02-18 14:06:28 -0500369 mMagnetizedBubbleDraggingOut = null;
370
Lyn Han522e9ff2019-05-17 13:26:13 -0700371 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500372 }
373
Joshua Tsujif49ee142019-05-29 16:32:01 -0400374 /** Resets bubble drag out gesture flags. */
375 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500376 mBubbleDraggedOutEnough = false;
Lyn Hanf44562b2020-03-30 16:40:46 -0700377 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400378 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500379 }
380
381 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800382 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
383 */
384 public void updateYPosition(Runnable after) {
385 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500386 animationsForChildrenFromIndex(
387 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800388 }
389
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800390 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800391 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800392 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
393 return 0;
394 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800395 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han4a8efe32019-05-30 09:43:27 -0700396 return mBubblePaddingTop + Math.max(
Lyn Hanc47e1712020-01-28 21:43:34 -0800397 mStatusBarHeight,
398 insets.getDisplayCutout() != null
399 ? insets.getDisplayCutout().getSafeInsetTop()
400 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800401 }
402
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400403 /** Description of current animation controller state. */
404 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
405 pw.println("ExpandedAnimationController state:");
406 pw.print(" isActive: "); pw.println(isActiveController());
407 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
408 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400409 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
410 }
411
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800412 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400413 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
414 final Resources res = layout.getResources();
415 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400416 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400417 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
418 mStatusBarHeight =
419 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400420 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400421
Lyn Hanc47e1712020-01-28 21:43:34 -0800422 // Includes overflow button.
Lyn Hanf418e782020-03-12 21:51:30 -0700423 float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2)
424 - (mBubblesMaxRendered + 1) * mBubbleSizePx;
Lyn Hanc47e1712020-01-28 21:43:34 -0800425 mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered;
426
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400427 // Ensure that all child views are at 1x scale, and visible, in case they were animating
428 // in.
429 mLayout.setVisibility(View.VISIBLE);
430 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
431 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
432 }
433
434 @Override
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800435 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
436 return Sets.newHashSet(
437 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500438 DynamicAnimation.TRANSLATION_Y,
439 DynamicAnimation.SCALE_X,
440 DynamicAnimation.SCALE_Y,
441 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800442 }
443
444 @Override
445 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
446 return NONE;
447 }
448
449 @Override
450 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
451 return 0;
452 }
453
454 @Override
455 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
456 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500457 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
458 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800459 }
460
461 @Override
462 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400463 // If a bubble is added while the expand/collapse animations are playing, update the
464 // animation to include the new bubble.
465 if (mAnimatingExpand) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400466 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400467 } else if (mAnimatingCollapse) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400468 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400469 } else {
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400470 child.setTranslationX(getBubbleLeft(index));
Joshua Tsujif49ee142019-05-29 16:32:01 -0400471 animationForChild(child)
472 .translationY(
473 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
474 getExpandedY() /* to */)
475 .start();
476 updateBubblePositions();
477 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800478 }
479
480 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500481 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500482 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500483
Joshua Tsuji442b6272019-02-08 13:23:43 -0500484 // If we're removing the dragged-out bubble, that means it got dismissed.
Joshua Tsuji20103542020-02-18 14:06:28 -0500485 if (child.equals(getDraggedOutBubble())) {
486 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400487 finishRemoval.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500488 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400489 animator.alpha(0f, finishRemoval /* endAction */)
490 .withStiffness(SpringForce.STIFFNESS_HIGH)
491 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
492 .scaleX(1.1f)
493 .scaleY(1.1f)
494 .start();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500495 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500496
497 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700498 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500499 }
500
501 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400502 void onChildReordered(View child, int oldIndex, int newIndex) {
503 updateBubblePositions();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400504
505 // We expect reordering during collapse, since we'll put the last selected bubble on top.
506 // Update the collapse animation so they end up in the right stacked positions.
507 if (mAnimatingCollapse) {
508 startOrUpdatePathAnimation(false /* expanding */);
509 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500510 }
511
Lyn Han522e9ff2019-05-17 13:26:13 -0700512 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400513 if (mAnimatingExpand || mAnimatingCollapse) {
514 return;
515 }
516
Lyn Han522e9ff2019-05-17 13:26:13 -0700517 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500518 final View bubble = mLayout.getChildAt(i);
519
520 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
521 // will be snapped to the correct X value after the drag (if it's not dismissed).
Joshua Tsuji20103542020-02-18 14:06:28 -0500522 if (bubble.equals(getDraggedOutBubble())) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700523 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500524 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400525
Lyn Han522e9ff2019-05-17 13:26:13 -0700526 animationForChild(bubble)
527 .translationX(getBubbleLeft(i))
528 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500529 }
530 }
531
Lyn Han522e9ff2019-05-17 13:26:13 -0700532 /**
533 * @param index Bubble index in row.
534 * @return Bubble left x from left edge of screen.
535 */
536 public float getBubbleLeft(int index) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800537 final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles);
Lyn Han4a8efe32019-05-30 09:43:27 -0700538 return getRowLeft() + bubbleFromRowLeft;
Lyn Han522e9ff2019-05-17 13:26:13 -0700539 }
540
Mady Mellore19353d2019-08-21 17:25:02 -0700541 /**
542 * When expanded, the bubbles are centered in the screen. In portrait, all available space is
543 * used. In landscape we have too much space so the value is restricted. This method accounts
544 * for window decorations (nav bar, cutouts).
545 *
546 * @return the desired width to display the expanded bubbles in.
547 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800548 public float getWidthForDisplayingBubbles() {
Mady Mellore19353d2019-08-21 17:25:02 -0700549 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
550 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
551 // display size y in landscape will be the smaller dimension of the screen
552 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
553 } else {
554 return availableWidth;
555 }
556 }
557
558 /**
559 * Determines the available screen width without the cutout.
560 *
561 * @param subtractStableInsets Whether or not stable insets should also be removed from the
Lyn Hanc47e1712020-01-28 21:43:34 -0800562 * returned width.
Mady Mellore19353d2019-08-21 17:25:02 -0700563 * @return the total screen width available accounting for cutouts and insets,
564 * iff {@param includeStableInsets} is true.
565 */
566 private float getAvailableScreenWidth(boolean subtractStableInsets) {
567 float availableSize = mDisplaySize.x;
568 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
569 if (insets != null) {
570 int cutoutLeft = 0;
571 int cutoutRight = 0;
572 DisplayCutout cutout = insets.getDisplayCutout();
573 if (cutout != null) {
574 cutoutLeft = cutout.getSafeInsetLeft();
575 cutoutRight = cutout.getSafeInsetRight();
576 }
577 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
578 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
579 availableSize -= Math.max(stableLeft, cutoutLeft);
580 availableSize -= Math.max(stableRight, cutoutRight);
581 }
582 return availableSize;
583 }
584
Lyn Han522e9ff2019-05-17 13:26:13 -0700585 private float getRowLeft() {
586 if (mLayout == null) {
587 return 0;
588 }
Lyn Hanc47e1712020-01-28 21:43:34 -0800589 float rowWidth = (mLayout.getChildCount() * mBubbleSizePx)
590 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
Lyn Han522e9ff2019-05-17 13:26:13 -0700591
Mady Mellore19353d2019-08-21 17:25:02 -0700592 // This display size we're using includes the size of the insets, we want the true
593 // center of the display minus the notch here, which means we should include the
594 // stable insets (e.g. status bar, nav bar) in this calculation.
595 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
Lyn Hanc47e1712020-01-28 21:43:34 -0800596 return trueCenter - (rowWidth / 2f);
Lyn Han522e9ff2019-05-17 13:26:13 -0700597 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800598}