blob: 8e232520a19699097507dec2cedc90d8506c4071 [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 Tsuji4395bbd2020-05-19 17:53:33 -040035import com.android.systemui.util.animation.PhysicsAnimator;
Joshua Tsuji20103542020-02-18 14:06:28 -050036import com.android.systemui.util.magnetictarget.MagnetizedObject;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080037
38import com.google.android.collect.Sets;
39
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040040import java.io.FileDescriptor;
41import java.io.PrintWriter;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080042import java.util.Set;
43
44/**
45 * Animation controller for bubbles when they're in their expanded state, or animating to/from the
46 * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
47 * dismissed.
48 */
49public class ExpandedAnimationController
50 extends PhysicsAnimationLayout.PhysicsAnimationController {
51
52 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050053 * How much to translate the bubbles when they're animating in/out. This value is multiplied by
54 * the bubble size.
55 */
56 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
57
Joshua Tsujidebd8312019-06-06 17:17:08 -040058 /** Duration of the expand/collapse target path animation. */
Joshua Tsuji06785ab2020-06-08 11:18:40 -040059 public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
Joshua Tsuji442b6272019-02-08 13:23:43 -050060
Joshua Tsujidebd8312019-06-06 17:17:08 -040061 /** Stiffness for the expand/collapse path-following animation. */
62 private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080063
Mady Mellore19353d2019-08-21 17:25:02 -070064 /** What percentage of the screen to use when centering the bubbles in landscape. */
65 private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
66
Joshua Tsuji20103542020-02-18 14:06:28 -050067 /**
68 * Velocity required to dismiss an individual bubble without dragging it into the dismiss
69 * target.
70 */
71 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
72
Joshua Tsuji4395bbd2020-05-19 17:53:33 -040073 private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig =
74 new PhysicsAnimator.SpringConfig(
75 EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
76
Joshua Tsujib1a796b2019-01-16 15:43:12 -080077 /** Horizontal offset between bubbles, which we need to know to re-stack them. */
78 private float mStackOffsetPx;
Lyn Han4a8efe32019-05-30 09:43:27 -070079 /** Space between status bar and bubbles in the expanded state. */
80 private float mBubblePaddingTop;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080081 /** Size of each bubble. */
82 private float mBubbleSizePx;
Lyn Hanc47e1712020-01-28 21:43:34 -080083 /** Space between bubbles in row above expanded view. */
84 private float mSpaceBetweenBubbles;
Joshua Tsujif44347f2019-02-12 14:28:06 -050085 /** Height of the status bar. */
86 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080087 /** Size of display. */
88 private Point mDisplaySize;
Lyn Hanc47e1712020-01-28 21:43:34 -080089 /** Max number of bubbles shown in row above expanded view. */
Lyn Han522e9ff2019-05-17 13:26:13 -070090 private int mBubblesMaxRendered;
Mady Mellore19353d2019-08-21 17:25:02 -070091 /** What the current screen orientation is. */
92 private int mScreenOrientation;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080093
Joshua Tsujif49ee142019-05-29 16:32:01 -040094 private boolean mAnimatingExpand = false;
Josh Tsujid12d46a2020-06-12 12:51:19 -040095
96 /**
97 * Whether we are animating other Bubbles UI elements out in preparation for a call to
98 * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or
99 * reorders.
100 */
101 private boolean mPreparingToCollapse = false;
102
Joshua Tsujif49ee142019-05-29 16:32:01 -0400103 private boolean mAnimatingCollapse = false;
Lyn Hanb58c7562020-01-07 14:29:20 -0800104 private @Nullable Runnable mAfterExpand;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400105 private Runnable mAfterCollapse;
106 private PointF mCollapsePoint;
107
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400108 /**
109 * Whether the dragged out bubble is springing towards the touch point, rather than using the
110 * default behavior of moving directly to the touch point.
111 *
112 * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
113 * the center. Since the touch point differs from the bubble location, we need to animate the
114 * bubble back to the touch point to avoid a jarring instant location change from the center of
115 * the target to the touch point just outside the target bounds.
116 */
117 private boolean mSpringingBubbleToTouch = false;
118
Joshua Tsuji20103542020-02-18 14:06:28 -0500119 /**
120 * Whether to spring the bubble to the next touch event coordinates. This is used to animate the
121 * bubble out of the magnetic dismiss target to the touch location.
122 *
123 * Once it 'catches up' and the animation ends, we'll revert to moving it directly.
124 */
125 private boolean mSpringToTouchOnNextMotionEvent = false;
126
127 /** The bubble currently being dragged out of the row (to potentially be dismissed). */
128 private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
129
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700130 private int mExpandedViewPadding;
131
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400132 /**
133 * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the
134 * end of this animation means we have no bubbles left, and notify the BubbleController.
135 */
136 private Runnable mOnBubbleAnimatedOutAction;
137
Lyn Hanf4730312019-06-18 11:18:58 -0700138 public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400139 int orientation, Runnable onBubbleAnimatedOutAction) {
Lyn Hanb4b06132020-05-11 09:25:20 -0700140 updateResources(orientation, displaySize);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700141 mExpandedViewPadding = expandedViewPadding;
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400142 mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800143 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800144
Joshua Tsuji442b6272019-02-08 13:23:43 -0500145 /**
146 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
147 * the rest of the bubbles to animate to fill the gap.
148 */
149 private boolean mBubbleDraggedOutEnough = false;
150
Joshua Tsuji5084b662020-04-10 12:54:05 -0400151 /** End action to run when the lead bubble's expansion animation completes. */
152 @Nullable private Runnable mLeadBubbleEndAction;
153
154 /**
155 * Animates expanding the bubbles into a row along the top of the screen, optionally running an
156 * end action when the entire animation completes, and an end action when the lead bubble's
157 * animation ends.
158 */
159 public void expandFromStack(
160 @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) {
Josh Tsujid12d46a2020-06-12 12:51:19 -0400161 mPreparingToCollapse = false;
Joshua Tsuji5084b662020-04-10 12:54:05 -0400162 mAnimatingCollapse = false;
163 mAnimatingExpand = true;
164 mAfterExpand = after;
165 mLeadBubbleEndAction = leadBubbleEndAction;
166
167 startOrUpdatePathAnimation(true /* expanding */);
168 }
169
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800170 /**
171 * Animates expanding the bubbles into a row along the top of the screen.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800172 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800173 public void expandFromStack(@Nullable Runnable after) {
Joshua Tsuji5084b662020-04-10 12:54:05 -0400174 expandFromStack(after, null /* leadBubbleEndAction */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800175 }
176
Josh Tsujid12d46a2020-06-12 12:51:19 -0400177 /**
178 * Sets that we're animating the stack collapsed, but haven't yet called
179 * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are
180 * added or re-ordered, since the upcoming collapse animation will handle positioning those
181 * bubbles in the collapsed stack.
182 */
183 public void notifyPreparingToCollapse() {
184 mPreparingToCollapse = true;
185 }
186
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800187 /** Animate collapsing the bubbles back to their stacked position. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400188 public void collapseBackToStack(PointF collapsePoint, Runnable after) {
189 mAnimatingExpand = false;
Josh Tsujid12d46a2020-06-12 12:51:19 -0400190 mPreparingToCollapse = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400191 mAnimatingCollapse = true;
192 mAfterCollapse = after;
193 mCollapsePoint = collapsePoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800194
Joshua Tsujidebd8312019-06-06 17:17:08 -0400195 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400196 }
197
Lyn Hanf4730312019-06-18 11:18:58 -0700198 /**
199 * Update effective screen width based on current orientation.
200 * @param orientation Landscape or portrait.
Mady Mellore19353d2019-08-21 17:25:02 -0700201 * @param displaySize Updated display size.
Lyn Hanf4730312019-06-18 11:18:58 -0700202 */
Lyn Hanb4b06132020-05-11 09:25:20 -0700203 public void updateResources(int orientation, Point displaySize) {
Mady Mellore19353d2019-08-21 17:25:02 -0700204 mScreenOrientation = orientation;
205 mDisplaySize = displaySize;
Lyn Han55f53a52020-06-12 01:19:05 -0700206 if (mLayout == null) {
207 return;
Mady Mellor818eef02019-08-16 16:12:29 -0700208 }
Lyn Han55f53a52020-06-12 01:19:05 -0700209 Resources res = mLayout.getContext().getResources();
210 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
211 mStatusBarHeight = res.getDimensionPixelSize(
212 com.android.internal.R.dimen.status_bar_height);
213 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
214 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
215 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
216 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
217
218 // Includes overflow button.
219 float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2)
220 - (mBubblesMaxRendered + 1) * mBubbleSizePx;
221 mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered;
Lyn Hanf4730312019-06-18 11:18:58 -0700222 }
223
Joshua Tsujidebd8312019-06-06 17:17:08 -0400224 /**
225 * Animates the bubbles along a curved path, either to expand them along the top or collapse
226 * them back into a stack.
227 */
228 private void startOrUpdatePathAnimation(boolean expanding) {
229 Runnable after;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400230
Joshua Tsujidebd8312019-06-06 17:17:08 -0400231 if (expanding) {
232 after = () -> {
233 mAnimatingExpand = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400234
Joshua Tsujidebd8312019-06-06 17:17:08 -0400235 if (mAfterExpand != null) {
236 mAfterExpand.run();
237 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400238
Joshua Tsujidebd8312019-06-06 17:17:08 -0400239 mAfterExpand = null;
240 };
241 } else {
242 after = () -> {
243 mAnimatingCollapse = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400244
Joshua Tsujidebd8312019-06-06 17:17:08 -0400245 if (mAfterCollapse != null) {
246 mAfterCollapse.run();
247 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400248
Joshua Tsujidebd8312019-06-06 17:17:08 -0400249 mAfterCollapse = null;
250 };
251 }
252
253 // Animate each bubble individually, since each path will end in a different spot.
254 animationsForChildrenFromIndex(0, (index, animation) -> {
255 final View bubble = mLayout.getChildAt(index);
256
257 // Start a path at the bubble's current position.
258 final Path path = new Path();
259 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
260
261 final float expandedY = getExpandedY();
262 if (expanding) {
263 // If we're expanding, first draw a line from the bubble's current position to the
264 // top of the screen.
265 path.lineTo(bubble.getTranslationX(), expandedY);
266
267 // Then, draw a line across the screen to the bubble's resting position.
268 path.lineTo(getBubbleLeft(index), expandedY);
269 } else {
270 final float sideMultiplier =
271 mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
272 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
273
274 // If we're collapsing, draw a line from the bubble's current position to the side
275 // of the screen where the bubble will be stacked.
276 path.lineTo(stackedX, expandedY);
277
278 // Then, draw a line down to the stack position.
279 path.lineTo(stackedX, mCollapsePoint.y);
280 }
281
282 // The lead bubble should be the bubble with the longest distance to travel when we're
283 // expanding, and the bubble with the shortest distance to travel when we're collapsing.
284 // During expansion from the left side, the last bubble has to travel to the far right
285 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
286 // right side, the first bubble is traveling to the top left, so it leads. During
287 // collapse to the left, the first bubble has the shortest travel time back to the stack
288 // position, so it leads (and vice versa).
289 final boolean firstBubbleLeads =
290 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
291 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
292 final int startDelay = firstBubbleLeads
293 ? (index * 10)
294 : ((mLayout.getChildCount() - index) * 10);
295
Joshua Tsuji5084b662020-04-10 12:54:05 -0400296 final boolean isLeadBubble =
297 (firstBubbleLeads && index == 0)
298 || (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
299
Joshua Tsujidebd8312019-06-06 17:17:08 -0400300 animation
301 .followAnimatedTargetAlongPath(
302 path,
303 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
Joshua Tsuji5084b662020-04-10 12:54:05 -0400304 Interpolators.LINEAR /* targetAnimInterpolator */,
305 isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
306 () -> mLeadBubbleEndAction = null /* endAction */)
Joshua Tsujidebd8312019-06-06 17:17:08 -0400307 .withStartDelay(startDelay)
308 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
309 }).startAll(after);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800310 }
311
Joshua Tsuji20103542020-02-18 14:06:28 -0500312 /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
313 public void onUnstuckFromTarget() {
314 mSpringToTouchOnNextMotionEvent = true;
315 }
316
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400317 /**
318 * Prepares the given bubble view to be dragged out, using the provided magnetic target and
319 * listener.
320 */
321 public void prepareForBubbleDrag(
322 View bubble,
323 MagnetizedObject.MagneticTarget target,
324 MagnetizedObject.MagnetListener listener) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500325 mLayout.cancelAnimationsOnView(bubble);
326
Joshua Tsuji20103542020-02-18 14:06:28 -0500327 bubble.setTranslationZ(Short.MAX_VALUE);
328 mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
329 mLayout.getContext(), bubble,
330 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
331 @Override
332 public float getWidth(@NonNull View underlyingObject) {
333 return mBubbleSizePx;
334 }
335
336 @Override
337 public float getHeight(@NonNull View underlyingObject) {
338 return mBubbleSizePx;
339 }
340
341 @Override
342 public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
343 loc[0] = (int) bubble.getTranslationX();
344 loc[1] = (int) bubble.getTranslationY();
345 }
346 };
347 mMagnetizedBubbleDraggingOut.addTarget(target);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400348 mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
Joshua Tsuji20103542020-02-18 14:06:28 -0500349 mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
350 mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
351 }
352
353 private void springBubbleTo(View bubble, float x, float y) {
354 animationForChild(bubble)
355 .translationX(x)
356 .translationY(y)
357 .withStiffness(SpringForce.STIFFNESS_HIGH)
358 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500359 }
360
361 /**
362 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
363 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
364 * bubble is dragged back into the row.
365 */
366 public void dragBubbleOut(View bubbleView, float x, float y) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500367 if (mSpringToTouchOnNextMotionEvent) {
368 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
369 mSpringToTouchOnNextMotionEvent = false;
370 mSpringingBubbleToTouch = true;
371 } else if (mSpringingBubbleToTouch) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400372 if (mLayout.arePropertiesAnimatingOnView(
373 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500374 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400375 } else {
376 mSpringingBubbleToTouch = false;
377 }
378 }
379
Joshua Tsuji20103542020-02-18 14:06:28 -0500380 if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400381 bubbleView.setTranslationX(x);
382 bubbleView.setTranslationY(y);
383 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500384
385 final boolean draggedOutEnough =
386 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
387 if (draggedOutEnough != mBubbleDraggedOutEnough) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700388 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500389 mBubbleDraggedOutEnough = draggedOutEnough;
390 }
391 }
392
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400393 /** Plays a dismiss animation on the dragged out bubble. */
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400394 public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) {
Mady Mellor12c90952020-04-06 12:29:07 -0700395 if (bubble == null) {
396 return;
397 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400398 animationForChild(bubble)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400399 .withStiffness(SpringForce.STIFFNESS_HIGH)
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400400 .scaleX(0f)
401 .scaleY(0f)
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400402 .translationY(bubble.getTranslationY() + translationYBy)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400403 .alpha(0f, after)
404 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700405
406 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400407 }
408
Joshua Tsujif49ee142019-05-29 16:32:01 -0400409 @Nullable public View getDraggedOutBubble() {
Joshua Tsuji20103542020-02-18 14:06:28 -0500410 return mMagnetizedBubbleDraggingOut == null
411 ? null
412 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
Joshua Tsujif49ee142019-05-29 16:32:01 -0400413 }
414
Joshua Tsuji20103542020-02-18 14:06:28 -0500415 /** Returns the MagnetizedObject instance for the dragging-out bubble. */
416 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
417 return mMagnetizedBubbleDraggingOut;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400418 }
419
Joshua Tsuji442b6272019-02-08 13:23:43 -0500420 /**
421 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
422 * bubbles to accommodate it if it was previously dragged out past the threshold.
423 */
424 public void snapBubbleBack(View bubbleView, float velX, float velY) {
425 final int index = mLayout.indexOfChild(bubbleView);
426
Joshua Tsujic1108432019-02-22 16:10:12 -0500427 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400428 .position(getBubbleLeft(index), getExpandedY())
429 .withPositionStartVelocities(velX, velY)
430 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500431
Joshua Tsuji20103542020-02-18 14:06:28 -0500432 mMagnetizedBubbleDraggingOut = null;
433
Lyn Han522e9ff2019-05-17 13:26:13 -0700434 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500435 }
436
Joshua Tsujif49ee142019-05-29 16:32:01 -0400437 /** Resets bubble drag out gesture flags. */
438 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500439 mBubbleDraggedOutEnough = false;
Lyn Hanf44562b2020-03-30 16:40:46 -0700440 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400441 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500442 }
443
444 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800445 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
446 */
447 public void updateYPosition(Runnable after) {
448 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500449 animationsForChildrenFromIndex(
450 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800451 }
452
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800453 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800454 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800455 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
456 return 0;
457 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800458 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han4a8efe32019-05-30 09:43:27 -0700459 return mBubblePaddingTop + Math.max(
Lyn Hanc47e1712020-01-28 21:43:34 -0800460 mStatusBarHeight,
461 insets.getDisplayCutout() != null
462 ? insets.getDisplayCutout().getSafeInsetTop()
463 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800464 }
465
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400466 /** Description of current animation controller state. */
467 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
468 pw.println("ExpandedAnimationController state:");
469 pw.print(" isActive: "); pw.println(isActiveController());
470 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
471 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400472 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
473 }
474
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800475 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400476 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
Lyn Han55f53a52020-06-12 01:19:05 -0700477 updateResources(mScreenOrientation, mDisplaySize);
Lyn Hanc47e1712020-01-28 21:43:34 -0800478
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400479 // Ensure that all child views are at 1x scale, and visible, in case they were animating
480 // in.
481 mLayout.setVisibility(View.VISIBLE);
482 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
483 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
484 }
485
486 @Override
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800487 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
488 return Sets.newHashSet(
489 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500490 DynamicAnimation.TRANSLATION_Y,
491 DynamicAnimation.SCALE_X,
492 DynamicAnimation.SCALE_Y,
493 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800494 }
495
496 @Override
497 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
498 return NONE;
499 }
500
501 @Override
502 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
503 return 0;
504 }
505
506 @Override
507 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
508 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500509 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
510 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800511 }
512
513 @Override
514 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400515 // If a bubble is added while the expand/collapse animations are playing, update the
516 // animation to include the new bubble.
517 if (mAnimatingExpand) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400518 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400519 } else if (mAnimatingCollapse) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400520 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400521 } else {
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400522 child.setTranslationX(getBubbleLeft(index));
Josh Tsujid12d46a2020-06-12 12:51:19 -0400523
524 // If we're preparing to collapse, don't start animations since the collapse animation
525 // will take over and animate the new bubble into the correct (stacked) position.
526 if (!mPreparingToCollapse) {
527 animationForChild(child)
528 .translationY(
529 getExpandedY()
530 - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
531 getExpandedY() /* to */)
532 .start();
533 updateBubblePositions();
534 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400535 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800536 }
537
538 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500539 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500540 // If we're removing the dragged-out bubble, that means it got dismissed.
Joshua Tsuji20103542020-02-18 14:06:28 -0500541 if (child.equals(getDraggedOutBubble())) {
542 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400543 finishRemoval.run();
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400544 mOnBubbleAnimatedOutAction.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500545 } else {
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400546 PhysicsAnimator.getInstance(child)
547 .spring(DynamicAnimation.ALPHA, 0f)
548 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
549 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
550 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400551 .start();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500552 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500553
554 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700555 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500556 }
557
558 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400559 void onChildReordered(View child, int oldIndex, int newIndex) {
Josh Tsujid12d46a2020-06-12 12:51:19 -0400560 if (mPreparingToCollapse) {
561 // If a re-order is received while we're preparing to collapse, ignore it. Once started,
562 // the collapse animation will animate all of the bubbles to their correct (stacked)
563 // position.
564 return;
565 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400566
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400567 if (mAnimatingCollapse) {
Josh Tsujid12d46a2020-06-12 12:51:19 -0400568 // If a re-order is received during collapse, update the animation so that the bubbles
569 // end up in the correct (stacked) position.
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400570 startOrUpdatePathAnimation(false /* expanding */);
Josh Tsujid12d46a2020-06-12 12:51:19 -0400571 } else {
572 // Otherwise, animate the bubbles around to reflect their new order.
573 updateBubblePositions();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400574 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500575 }
576
Lyn Han522e9ff2019-05-17 13:26:13 -0700577 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400578 if (mAnimatingExpand || mAnimatingCollapse) {
579 return;
580 }
581
Lyn Han522e9ff2019-05-17 13:26:13 -0700582 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500583 final View bubble = mLayout.getChildAt(i);
584
585 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
586 // will be snapped to the correct X value after the drag (if it's not dismissed).
Joshua Tsuji20103542020-02-18 14:06:28 -0500587 if (bubble.equals(getDraggedOutBubble())) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700588 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500589 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400590
Lyn Han522e9ff2019-05-17 13:26:13 -0700591 animationForChild(bubble)
592 .translationX(getBubbleLeft(i))
593 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500594 }
595 }
596
Lyn Han522e9ff2019-05-17 13:26:13 -0700597 /**
598 * @param index Bubble index in row.
599 * @return Bubble left x from left edge of screen.
600 */
601 public float getBubbleLeft(int index) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800602 final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles);
Lyn Han4a8efe32019-05-30 09:43:27 -0700603 return getRowLeft() + bubbleFromRowLeft;
Lyn Han522e9ff2019-05-17 13:26:13 -0700604 }
605
Mady Mellore19353d2019-08-21 17:25:02 -0700606 /**
607 * When expanded, the bubbles are centered in the screen. In portrait, all available space is
608 * used. In landscape we have too much space so the value is restricted. This method accounts
609 * for window decorations (nav bar, cutouts).
610 *
611 * @return the desired width to display the expanded bubbles in.
612 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800613 public float getWidthForDisplayingBubbles() {
Mady Mellore19353d2019-08-21 17:25:02 -0700614 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
615 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
616 // display size y in landscape will be the smaller dimension of the screen
617 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
618 } else {
619 return availableWidth;
620 }
621 }
622
623 /**
624 * Determines the available screen width without the cutout.
625 *
626 * @param subtractStableInsets Whether or not stable insets should also be removed from the
Lyn Hanc47e1712020-01-28 21:43:34 -0800627 * returned width.
Mady Mellore19353d2019-08-21 17:25:02 -0700628 * @return the total screen width available accounting for cutouts and insets,
629 * iff {@param includeStableInsets} is true.
630 */
631 private float getAvailableScreenWidth(boolean subtractStableInsets) {
632 float availableSize = mDisplaySize.x;
633 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
634 if (insets != null) {
635 int cutoutLeft = 0;
636 int cutoutRight = 0;
637 DisplayCutout cutout = insets.getDisplayCutout();
638 if (cutout != null) {
639 cutoutLeft = cutout.getSafeInsetLeft();
640 cutoutRight = cutout.getSafeInsetRight();
641 }
642 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
643 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
644 availableSize -= Math.max(stableLeft, cutoutLeft);
645 availableSize -= Math.max(stableRight, cutoutRight);
646 }
647 return availableSize;
648 }
649
Lyn Han522e9ff2019-05-17 13:26:13 -0700650 private float getRowLeft() {
651 if (mLayout == null) {
652 return 0;
653 }
Lyn Hanc47e1712020-01-28 21:43:34 -0800654 float rowWidth = (mLayout.getChildCount() * mBubbleSizePx)
655 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
Lyn Han522e9ff2019-05-17 13:26:13 -0700656
Mady Mellore19353d2019-08-21 17:25:02 -0700657 // This display size we're using includes the size of the insets, we want the true
658 // center of the display minus the notch here, which means we should include the
659 // stable insets (e.g. status bar, nav bar) in this calculation.
660 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
Lyn Hanc47e1712020-01-28 21:43:34 -0800661 return trueCenter - (rowWidth / 2f);
Lyn Han522e9ff2019-05-17 13:26:13 -0700662 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800663}