blob: 0002e862bb4163b1679be4d725e5fb63977fc005 [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 Tsuji79a58ee2020-03-27 17:55:37 -0400332 public void dismissDraggedOutBubble(View bubble, float translationYBy, 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)
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400340 .translationY(bubble.getTranslationY() + translationYBy)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400341 .alpha(0f, after)
342 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700343
344 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400345 }
346
Joshua Tsujif49ee142019-05-29 16:32:01 -0400347 @Nullable public View getDraggedOutBubble() {
Joshua Tsuji20103542020-02-18 14:06:28 -0500348 return mMagnetizedBubbleDraggingOut == null
349 ? null
350 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
Joshua Tsujif49ee142019-05-29 16:32:01 -0400351 }
352
Joshua Tsuji20103542020-02-18 14:06:28 -0500353 /** Returns the MagnetizedObject instance for the dragging-out bubble. */
354 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
355 return mMagnetizedBubbleDraggingOut;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400356 }
357
Joshua Tsuji442b6272019-02-08 13:23:43 -0500358 /**
359 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
360 * bubbles to accommodate it if it was previously dragged out past the threshold.
361 */
362 public void snapBubbleBack(View bubbleView, float velX, float velY) {
363 final int index = mLayout.indexOfChild(bubbleView);
364
Joshua Tsujic1108432019-02-22 16:10:12 -0500365 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400366 .position(getBubbleLeft(index), getExpandedY())
367 .withPositionStartVelocities(velX, velY)
368 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500369
Joshua Tsuji20103542020-02-18 14:06:28 -0500370 mMagnetizedBubbleDraggingOut = null;
371
Lyn Han522e9ff2019-05-17 13:26:13 -0700372 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500373 }
374
Joshua Tsujif49ee142019-05-29 16:32:01 -0400375 /** Resets bubble drag out gesture flags. */
376 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500377 mBubbleDraggedOutEnough = false;
Lyn Hanf44562b2020-03-30 16:40:46 -0700378 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400379 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500380 }
381
382 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800383 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
384 */
385 public void updateYPosition(Runnable after) {
386 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500387 animationsForChildrenFromIndex(
388 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800389 }
390
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800391 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800392 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800393 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
394 return 0;
395 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800396 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han4a8efe32019-05-30 09:43:27 -0700397 return mBubblePaddingTop + Math.max(
Lyn Hanc47e1712020-01-28 21:43:34 -0800398 mStatusBarHeight,
399 insets.getDisplayCutout() != null
400 ? insets.getDisplayCutout().getSafeInsetTop()
401 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800402 }
403
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400404 /** Description of current animation controller state. */
405 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
406 pw.println("ExpandedAnimationController state:");
407 pw.print(" isActive: "); pw.println(isActiveController());
408 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
409 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400410 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
411 }
412
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800413 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400414 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
415 final Resources res = layout.getResources();
416 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400417 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400418 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
419 mStatusBarHeight =
420 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400421 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400422
Lyn Hanc47e1712020-01-28 21:43:34 -0800423 // Includes overflow button.
Lyn Hanf418e782020-03-12 21:51:30 -0700424 float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2)
425 - (mBubblesMaxRendered + 1) * mBubbleSizePx;
Lyn Hanc47e1712020-01-28 21:43:34 -0800426 mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered;
427
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400428 // Ensure that all child views are at 1x scale, and visible, in case they were animating
429 // in.
430 mLayout.setVisibility(View.VISIBLE);
431 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
432 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
433 }
434
435 @Override
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800436 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
437 return Sets.newHashSet(
438 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500439 DynamicAnimation.TRANSLATION_Y,
440 DynamicAnimation.SCALE_X,
441 DynamicAnimation.SCALE_Y,
442 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800443 }
444
445 @Override
446 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
447 return NONE;
448 }
449
450 @Override
451 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
452 return 0;
453 }
454
455 @Override
456 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
457 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500458 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
459 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800460 }
461
462 @Override
463 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400464 // If a bubble is added while the expand/collapse animations are playing, update the
465 // animation to include the new bubble.
466 if (mAnimatingExpand) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400467 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400468 } else if (mAnimatingCollapse) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400469 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400470 } else {
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400471 child.setTranslationX(getBubbleLeft(index));
Joshua Tsujif49ee142019-05-29 16:32:01 -0400472 animationForChild(child)
473 .translationY(
474 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
475 getExpandedY() /* to */)
476 .start();
477 updateBubblePositions();
478 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800479 }
480
481 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500482 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500483 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500484
Joshua Tsuji442b6272019-02-08 13:23:43 -0500485 // If we're removing the dragged-out bubble, that means it got dismissed.
Joshua Tsuji20103542020-02-18 14:06:28 -0500486 if (child.equals(getDraggedOutBubble())) {
487 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400488 finishRemoval.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500489 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400490 animator.alpha(0f, finishRemoval /* endAction */)
491 .withStiffness(SpringForce.STIFFNESS_HIGH)
492 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
493 .scaleX(1.1f)
494 .scaleY(1.1f)
495 .start();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500496 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500497
498 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700499 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500500 }
501
502 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400503 void onChildReordered(View child, int oldIndex, int newIndex) {
504 updateBubblePositions();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400505
506 // We expect reordering during collapse, since we'll put the last selected bubble on top.
507 // Update the collapse animation so they end up in the right stacked positions.
508 if (mAnimatingCollapse) {
509 startOrUpdatePathAnimation(false /* expanding */);
510 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500511 }
512
Lyn Han522e9ff2019-05-17 13:26:13 -0700513 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400514 if (mAnimatingExpand || mAnimatingCollapse) {
515 return;
516 }
517
Lyn Han522e9ff2019-05-17 13:26:13 -0700518 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500519 final View bubble = mLayout.getChildAt(i);
520
521 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
522 // will be snapped to the correct X value after the drag (if it's not dismissed).
Joshua Tsuji20103542020-02-18 14:06:28 -0500523 if (bubble.equals(getDraggedOutBubble())) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700524 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500525 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400526
Lyn Han522e9ff2019-05-17 13:26:13 -0700527 animationForChild(bubble)
528 .translationX(getBubbleLeft(i))
529 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500530 }
531 }
532
Lyn Han522e9ff2019-05-17 13:26:13 -0700533 /**
534 * @param index Bubble index in row.
535 * @return Bubble left x from left edge of screen.
536 */
537 public float getBubbleLeft(int index) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800538 final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles);
Lyn Han4a8efe32019-05-30 09:43:27 -0700539 return getRowLeft() + bubbleFromRowLeft;
Lyn Han522e9ff2019-05-17 13:26:13 -0700540 }
541
Mady Mellore19353d2019-08-21 17:25:02 -0700542 /**
543 * When expanded, the bubbles are centered in the screen. In portrait, all available space is
544 * used. In landscape we have too much space so the value is restricted. This method accounts
545 * for window decorations (nav bar, cutouts).
546 *
547 * @return the desired width to display the expanded bubbles in.
548 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800549 public float getWidthForDisplayingBubbles() {
Mady Mellore19353d2019-08-21 17:25:02 -0700550 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
551 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
552 // display size y in landscape will be the smaller dimension of the screen
553 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
554 } else {
555 return availableWidth;
556 }
557 }
558
559 /**
560 * Determines the available screen width without the cutout.
561 *
562 * @param subtractStableInsets Whether or not stable insets should also be removed from the
Lyn Hanc47e1712020-01-28 21:43:34 -0800563 * returned width.
Mady Mellore19353d2019-08-21 17:25:02 -0700564 * @return the total screen width available accounting for cutouts and insets,
565 * iff {@param includeStableInsets} is true.
566 */
567 private float getAvailableScreenWidth(boolean subtractStableInsets) {
568 float availableSize = mDisplaySize.x;
569 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
570 if (insets != null) {
571 int cutoutLeft = 0;
572 int cutoutRight = 0;
573 DisplayCutout cutout = insets.getDisplayCutout();
574 if (cutout != null) {
575 cutoutLeft = cutout.getSafeInsetLeft();
576 cutoutRight = cutout.getSafeInsetRight();
577 }
578 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
579 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
580 availableSize -= Math.max(stableLeft, cutoutLeft);
581 availableSize -= Math.max(stableRight, cutoutRight);
582 }
583 return availableSize;
584 }
585
Lyn Han522e9ff2019-05-17 13:26:13 -0700586 private float getRowLeft() {
587 if (mLayout == null) {
588 return 0;
589 }
Lyn Hanc47e1712020-01-28 21:43:34 -0800590 float rowWidth = (mLayout.getChildCount() * mBubbleSizePx)
591 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
Lyn Han522e9ff2019-05-17 13:26:13 -0700592
Mady Mellore19353d2019-08-21 17:25:02 -0700593 // This display size we're using includes the size of the insets, we want the true
594 // center of the display minus the notch here, which means we should include the
595 // stable insets (e.g. status bar, nav bar) in this calculation.
596 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
Lyn Hanc47e1712020-01-28 21:43:34 -0800597 return trueCenter - (rowWidth / 2f);
Lyn Han522e9ff2019-05-17 13:26:13 -0700598 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800599}