blob: f57cf42ce4ffa542a2cbbfcf2e6f5c2c3cf192f4 [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) {
Lyn Hanb4b06132020-05-11 09:25:20 -0700121 updateResources(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 Tsuji5084b662020-04-10 12:54:05 -0400131 /** End action to run when the lead bubble's expansion animation completes. */
132 @Nullable private Runnable mLeadBubbleEndAction;
133
134 /**
135 * Animates expanding the bubbles into a row along the top of the screen, optionally running an
136 * end action when the entire animation completes, and an end action when the lead bubble's
137 * animation ends.
138 */
139 public void expandFromStack(
140 @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) {
141 mAnimatingCollapse = false;
142 mAnimatingExpand = true;
143 mAfterExpand = after;
144 mLeadBubbleEndAction = leadBubbleEndAction;
145
146 startOrUpdatePathAnimation(true /* expanding */);
147 }
148
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800149 /**
150 * Animates expanding the bubbles into a row along the top of the screen.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800151 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800152 public void expandFromStack(@Nullable Runnable after) {
Joshua Tsuji5084b662020-04-10 12:54:05 -0400153 expandFromStack(after, null /* leadBubbleEndAction */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800154 }
155
156 /** Animate collapsing the bubbles back to their stacked position. */
Joshua Tsujif49ee142019-05-29 16:32:01 -0400157 public void collapseBackToStack(PointF collapsePoint, Runnable after) {
158 mAnimatingExpand = false;
159 mAnimatingCollapse = true;
160 mAfterCollapse = after;
161 mCollapsePoint = collapsePoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800162
Joshua Tsujidebd8312019-06-06 17:17:08 -0400163 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400164 }
165
Lyn Hanf4730312019-06-18 11:18:58 -0700166 /**
167 * Update effective screen width based on current orientation.
168 * @param orientation Landscape or portrait.
Mady Mellore19353d2019-08-21 17:25:02 -0700169 * @param displaySize Updated display size.
Lyn Hanf4730312019-06-18 11:18:58 -0700170 */
Lyn Hanb4b06132020-05-11 09:25:20 -0700171 public void updateResources(int orientation, Point displaySize) {
Mady Mellore19353d2019-08-21 17:25:02 -0700172 mScreenOrientation = orientation;
173 mDisplaySize = displaySize;
Mady Mellor818eef02019-08-16 16:12:29 -0700174 if (mLayout != null) {
175 Resources res = mLayout.getContext().getResources();
Mady Mellore19353d2019-08-21 17:25:02 -0700176 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellor818eef02019-08-16 16:12:29 -0700177 mStatusBarHeight = res.getDimensionPixelSize(
178 com.android.internal.R.dimen.status_bar_height);
Mady Mellor818eef02019-08-16 16:12:29 -0700179 }
Lyn Hanf4730312019-06-18 11:18:58 -0700180 }
181
Joshua Tsujidebd8312019-06-06 17:17:08 -0400182 /**
183 * Animates the bubbles along a curved path, either to expand them along the top or collapse
184 * them back into a stack.
185 */
186 private void startOrUpdatePathAnimation(boolean expanding) {
187 Runnable after;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400188
Joshua Tsujidebd8312019-06-06 17:17:08 -0400189 if (expanding) {
190 after = () -> {
191 mAnimatingExpand = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400192
Joshua Tsujidebd8312019-06-06 17:17:08 -0400193 if (mAfterExpand != null) {
194 mAfterExpand.run();
195 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400196
Joshua Tsujidebd8312019-06-06 17:17:08 -0400197 mAfterExpand = null;
198 };
199 } else {
200 after = () -> {
201 mAnimatingCollapse = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -0400202
Joshua Tsujidebd8312019-06-06 17:17:08 -0400203 if (mAfterCollapse != null) {
204 mAfterCollapse.run();
205 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400206
Joshua Tsujidebd8312019-06-06 17:17:08 -0400207 mAfterCollapse = null;
208 };
209 }
210
211 // Animate each bubble individually, since each path will end in a different spot.
212 animationsForChildrenFromIndex(0, (index, animation) -> {
213 final View bubble = mLayout.getChildAt(index);
214
215 // Start a path at the bubble's current position.
216 final Path path = new Path();
217 path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
218
219 final float expandedY = getExpandedY();
220 if (expanding) {
221 // If we're expanding, first draw a line from the bubble's current position to the
222 // top of the screen.
223 path.lineTo(bubble.getTranslationX(), expandedY);
224
225 // Then, draw a line across the screen to the bubble's resting position.
226 path.lineTo(getBubbleLeft(index), expandedY);
227 } else {
228 final float sideMultiplier =
229 mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
230 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
231
232 // If we're collapsing, draw a line from the bubble's current position to the side
233 // of the screen where the bubble will be stacked.
234 path.lineTo(stackedX, expandedY);
235
236 // Then, draw a line down to the stack position.
237 path.lineTo(stackedX, mCollapsePoint.y);
238 }
239
240 // The lead bubble should be the bubble with the longest distance to travel when we're
241 // expanding, and the bubble with the shortest distance to travel when we're collapsing.
242 // During expansion from the left side, the last bubble has to travel to the far right
243 // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
244 // right side, the first bubble is traveling to the top left, so it leads. During
245 // collapse to the left, the first bubble has the shortest travel time back to the stack
246 // position, so it leads (and vice versa).
247 final boolean firstBubbleLeads =
248 (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
249 || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
250 final int startDelay = firstBubbleLeads
251 ? (index * 10)
252 : ((mLayout.getChildCount() - index) * 10);
253
Joshua Tsuji5084b662020-04-10 12:54:05 -0400254 final boolean isLeadBubble =
255 (firstBubbleLeads && index == 0)
256 || (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
257
Joshua Tsujidebd8312019-06-06 17:17:08 -0400258 animation
259 .followAnimatedTargetAlongPath(
260 path,
261 EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
Joshua Tsuji5084b662020-04-10 12:54:05 -0400262 Interpolators.LINEAR /* targetAnimInterpolator */,
263 isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
264 () -> mLeadBubbleEndAction = null /* endAction */)
Joshua Tsujidebd8312019-06-06 17:17:08 -0400265 .withStartDelay(startDelay)
266 .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
267 }).startAll(after);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800268 }
269
Joshua Tsuji20103542020-02-18 14:06:28 -0500270 /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
271 public void onUnstuckFromTarget() {
272 mSpringToTouchOnNextMotionEvent = true;
273 }
274
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400275 /**
276 * Prepares the given bubble view to be dragged out, using the provided magnetic target and
277 * listener.
278 */
279 public void prepareForBubbleDrag(
280 View bubble,
281 MagnetizedObject.MagneticTarget target,
282 MagnetizedObject.MagnetListener listener) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500283 mLayout.cancelAnimationsOnView(bubble);
284
Joshua Tsuji20103542020-02-18 14:06:28 -0500285 bubble.setTranslationZ(Short.MAX_VALUE);
286 mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
287 mLayout.getContext(), bubble,
288 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
289 @Override
290 public float getWidth(@NonNull View underlyingObject) {
291 return mBubbleSizePx;
292 }
293
294 @Override
295 public float getHeight(@NonNull View underlyingObject) {
296 return mBubbleSizePx;
297 }
298
299 @Override
300 public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
301 loc[0] = (int) bubble.getTranslationX();
302 loc[1] = (int) bubble.getTranslationY();
303 }
304 };
305 mMagnetizedBubbleDraggingOut.addTarget(target);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400306 mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
Joshua Tsuji20103542020-02-18 14:06:28 -0500307 mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
308 mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
309 }
310
311 private void springBubbleTo(View bubble, float x, float y) {
312 animationForChild(bubble)
313 .translationX(x)
314 .translationY(y)
315 .withStiffness(SpringForce.STIFFNESS_HIGH)
316 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500317 }
318
319 /**
320 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
321 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
322 * bubble is dragged back into the row.
323 */
324 public void dragBubbleOut(View bubbleView, float x, float y) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500325 if (mSpringToTouchOnNextMotionEvent) {
326 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
327 mSpringToTouchOnNextMotionEvent = false;
328 mSpringingBubbleToTouch = true;
329 } else if (mSpringingBubbleToTouch) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400330 if (mLayout.arePropertiesAnimatingOnView(
331 bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500332 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400333 } else {
334 mSpringingBubbleToTouch = false;
335 }
336 }
337
Joshua Tsuji20103542020-02-18 14:06:28 -0500338 if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400339 bubbleView.setTranslationX(x);
340 bubbleView.setTranslationY(y);
341 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500342
343 final boolean draggedOutEnough =
344 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
345 if (draggedOutEnough != mBubbleDraggedOutEnough) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700346 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500347 mBubbleDraggedOutEnough = draggedOutEnough;
348 }
349 }
350
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400351 /** Plays a dismiss animation on the dragged out bubble. */
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400352 public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) {
Mady Mellor12c90952020-04-06 12:29:07 -0700353 if (bubble == null) {
354 return;
355 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400356 animationForChild(bubble)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400357 .withStiffness(SpringForce.STIFFNESS_HIGH)
358 .scaleX(1.1f)
359 .scaleY(1.1f)
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400360 .translationY(bubble.getTranslationY() + translationYBy)
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400361 .alpha(0f, after)
362 .start();
Lyn Han522e9ff2019-05-17 13:26:13 -0700363
364 updateBubblePositions();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400365 }
366
Joshua Tsujif49ee142019-05-29 16:32:01 -0400367 @Nullable public View getDraggedOutBubble() {
Joshua Tsuji20103542020-02-18 14:06:28 -0500368 return mMagnetizedBubbleDraggingOut == null
369 ? null
370 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
Joshua Tsujif49ee142019-05-29 16:32:01 -0400371 }
372
Joshua Tsuji20103542020-02-18 14:06:28 -0500373 /** Returns the MagnetizedObject instance for the dragging-out bubble. */
374 public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
375 return mMagnetizedBubbleDraggingOut;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400376 }
377
Joshua Tsuji442b6272019-02-08 13:23:43 -0500378 /**
379 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
380 * bubbles to accommodate it if it was previously dragged out past the threshold.
381 */
382 public void snapBubbleBack(View bubbleView, float velX, float velY) {
383 final int index = mLayout.indexOfChild(bubbleView);
384
Joshua Tsujic1108432019-02-22 16:10:12 -0500385 animationForChildAtIndex(index)
Joshua Tsujif49ee142019-05-29 16:32:01 -0400386 .position(getBubbleLeft(index), getExpandedY())
387 .withPositionStartVelocities(velX, velY)
388 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500389
Joshua Tsuji20103542020-02-18 14:06:28 -0500390 mMagnetizedBubbleDraggingOut = null;
391
Lyn Han522e9ff2019-05-17 13:26:13 -0700392 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500393 }
394
Joshua Tsujif49ee142019-05-29 16:32:01 -0400395 /** Resets bubble drag out gesture flags. */
396 public void onGestureFinished() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500397 mBubbleDraggedOutEnough = false;
Lyn Hanf44562b2020-03-30 16:40:46 -0700398 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400399 updateBubblePositions();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500400 }
401
402 /**
Mady Mellor5d8f1402019-02-21 18:23:52 -0800403 * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
404 */
405 public void updateYPosition(Runnable after) {
406 if (mLayout == null) return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500407 animationsForChildrenFromIndex(
408 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
Mady Mellor5d8f1402019-02-21 18:23:52 -0800409 }
410
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800411 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800412 public float getExpandedY() {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800413 if (mLayout == null || mLayout.getRootWindowInsets() == null) {
414 return 0;
415 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800416 final WindowInsets insets = mLayout.getRootWindowInsets();
Lyn Han4a8efe32019-05-30 09:43:27 -0700417 return mBubblePaddingTop + Math.max(
Lyn Hanc47e1712020-01-28 21:43:34 -0800418 mStatusBarHeight,
419 insets.getDisplayCutout() != null
420 ? insets.getDisplayCutout().getSafeInsetTop()
421 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800422 }
423
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400424 /** Description of current animation controller state. */
425 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
426 pw.println("ExpandedAnimationController state:");
427 pw.print(" isActive: "); pw.println(isActiveController());
428 pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
429 pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400430 pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
431 }
432
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800433 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400434 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
435 final Resources res = layout.getResources();
436 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400437 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400438 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
439 mStatusBarHeight =
440 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400441 mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400442
Lyn Hanc47e1712020-01-28 21:43:34 -0800443 // Includes overflow button.
Lyn Hanf418e782020-03-12 21:51:30 -0700444 float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2)
445 - (mBubblesMaxRendered + 1) * mBubbleSizePx;
Lyn Hanc47e1712020-01-28 21:43:34 -0800446 mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered;
447
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400448 // Ensure that all child views are at 1x scale, and visible, in case they were animating
449 // in.
450 mLayout.setVisibility(View.VISIBLE);
451 animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
452 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
453 }
454
455 @Override
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800456 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
457 return Sets.newHashSet(
458 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500459 DynamicAnimation.TRANSLATION_Y,
460 DynamicAnimation.SCALE_X,
461 DynamicAnimation.SCALE_Y,
462 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800463 }
464
465 @Override
466 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
467 return NONE;
468 }
469
470 @Override
471 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
472 return 0;
473 }
474
475 @Override
476 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
477 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500478 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
479 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800480 }
481
482 @Override
483 void onChildAdded(View child, int index) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400484 // If a bubble is added while the expand/collapse animations are playing, update the
485 // animation to include the new bubble.
486 if (mAnimatingExpand) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400487 startOrUpdatePathAnimation(true /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400488 } else if (mAnimatingCollapse) {
Joshua Tsujidebd8312019-06-06 17:17:08 -0400489 startOrUpdatePathAnimation(false /* expanding */);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400490 } else {
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400491 child.setTranslationX(getBubbleLeft(index));
Joshua Tsujif49ee142019-05-29 16:32:01 -0400492 animationForChild(child)
493 .translationY(
494 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
495 getExpandedY() /* to */)
496 .start();
497 updateBubblePositions();
498 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800499 }
500
501 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500502 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500503 final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500504
Joshua Tsuji442b6272019-02-08 13:23:43 -0500505 // If we're removing the dragged-out bubble, that means it got dismissed.
Joshua Tsuji20103542020-02-18 14:06:28 -0500506 if (child.equals(getDraggedOutBubble())) {
507 mMagnetizedBubbleDraggingOut = null;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400508 finishRemoval.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500509 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400510 animator.alpha(0f, finishRemoval /* endAction */)
511 .withStiffness(SpringForce.STIFFNESS_HIGH)
512 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
513 .scaleX(1.1f)
514 .scaleY(1.1f)
515 .start();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500516 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500517
518 // Animate all the other bubbles to their new positions sans this bubble.
Lyn Han522e9ff2019-05-17 13:26:13 -0700519 updateBubblePositions();
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500520 }
521
522 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400523 void onChildReordered(View child, int oldIndex, int newIndex) {
524 updateBubblePositions();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -0400525
526 // We expect reordering during collapse, since we'll put the last selected bubble on top.
527 // Update the collapse animation so they end up in the right stacked positions.
528 if (mAnimatingCollapse) {
529 startOrUpdatePathAnimation(false /* expanding */);
530 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500531 }
532
Lyn Han522e9ff2019-05-17 13:26:13 -0700533 private void updateBubblePositions() {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400534 if (mAnimatingExpand || mAnimatingCollapse) {
535 return;
536 }
537
Lyn Han522e9ff2019-05-17 13:26:13 -0700538 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500539 final View bubble = mLayout.getChildAt(i);
540
541 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
542 // will be snapped to the correct X value after the drag (if it's not dismissed).
Joshua Tsuji20103542020-02-18 14:06:28 -0500543 if (bubble.equals(getDraggedOutBubble())) {
Lyn Han522e9ff2019-05-17 13:26:13 -0700544 return;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500545 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400546
Lyn Han522e9ff2019-05-17 13:26:13 -0700547 animationForChild(bubble)
548 .translationX(getBubbleLeft(i))
549 .start();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500550 }
551 }
552
Lyn Han522e9ff2019-05-17 13:26:13 -0700553 /**
554 * @param index Bubble index in row.
555 * @return Bubble left x from left edge of screen.
556 */
557 public float getBubbleLeft(int index) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800558 final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles);
Lyn Han4a8efe32019-05-30 09:43:27 -0700559 return getRowLeft() + bubbleFromRowLeft;
Lyn Han522e9ff2019-05-17 13:26:13 -0700560 }
561
Mady Mellore19353d2019-08-21 17:25:02 -0700562 /**
563 * When expanded, the bubbles are centered in the screen. In portrait, all available space is
564 * used. In landscape we have too much space so the value is restricted. This method accounts
565 * for window decorations (nav bar, cutouts).
566 *
567 * @return the desired width to display the expanded bubbles in.
568 */
Lyn Hanb58c7562020-01-07 14:29:20 -0800569 public float getWidthForDisplayingBubbles() {
Mady Mellore19353d2019-08-21 17:25:02 -0700570 final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
571 if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
572 // display size y in landscape will be the smaller dimension of the screen
573 return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
574 } else {
575 return availableWidth;
576 }
577 }
578
579 /**
580 * Determines the available screen width without the cutout.
581 *
582 * @param subtractStableInsets Whether or not stable insets should also be removed from the
Lyn Hanc47e1712020-01-28 21:43:34 -0800583 * returned width.
Mady Mellore19353d2019-08-21 17:25:02 -0700584 * @return the total screen width available accounting for cutouts and insets,
585 * iff {@param includeStableInsets} is true.
586 */
587 private float getAvailableScreenWidth(boolean subtractStableInsets) {
588 float availableSize = mDisplaySize.x;
589 WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
590 if (insets != null) {
591 int cutoutLeft = 0;
592 int cutoutRight = 0;
593 DisplayCutout cutout = insets.getDisplayCutout();
594 if (cutout != null) {
595 cutoutLeft = cutout.getSafeInsetLeft();
596 cutoutRight = cutout.getSafeInsetRight();
597 }
598 final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
599 final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
600 availableSize -= Math.max(stableLeft, cutoutLeft);
601 availableSize -= Math.max(stableRight, cutoutRight);
602 }
603 return availableSize;
604 }
605
Lyn Han522e9ff2019-05-17 13:26:13 -0700606 private float getRowLeft() {
607 if (mLayout == null) {
608 return 0;
609 }
Lyn Hanc47e1712020-01-28 21:43:34 -0800610 float rowWidth = (mLayout.getChildCount() * mBubbleSizePx)
611 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
Lyn Han522e9ff2019-05-17 13:26:13 -0700612
Mady Mellore19353d2019-08-21 17:25:02 -0700613 // This display size we're using includes the size of the insets, we want the true
614 // center of the display minus the notch here, which means we should include the
615 // stable insets (e.g. status bar, nav bar) in this calculation.
616 final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
Lyn Hanc47e1712020-01-28 21:43:34 -0800617 return trueCenter - (rowWidth / 2f);
Lyn Han522e9ff2019-05-17 13:26:13 -0700618 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800619}