Fix issues with animations when the stack is expanded.
- Don't show the flyout if the stack has become expanded. - Add onChildReordered so that the controllers don't receive onChildAdded calls when views are reordered. This was causing animation issues with expanded bubbles. - Clear the dragging out bubble onGestureFinished, so that it's not ignored by animations. - Update the expand/collapse animations when a new child is added during animation, so that it'll also animate to the proper spot. Fixes: 129370170 Bug: 123542488 Test: atest SystemUITests Change-Id: I1360deb09db82bd3ba72cb91fc9abe05b6dc1a9c
This commit is contained in:
@@ -726,7 +726,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
public void updateBubbleOrder(List<Bubble> bubbles) {
|
||||
for (int i = 0; i < bubbles.size(); i++) {
|
||||
Bubble bubble = bubbles.get(i);
|
||||
mBubbleContainer.moveViewTo(bubble.iconView, i);
|
||||
mBubbleContainer.reorderView(bubble.iconView, i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,16 +908,14 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
if (shouldExpand) {
|
||||
mBubbleContainer.setActiveController(mExpandedAnimationController);
|
||||
mExpandedAnimationController.expandFromStack(
|
||||
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
|
||||
/* collapseTo */,
|
||||
() -> {
|
||||
updatePointerPosition();
|
||||
updateAfter.run();
|
||||
} /* after */);
|
||||
mExpandedAnimationController.expandFromStack(() -> {
|
||||
updatePointerPosition();
|
||||
updateAfter.run();
|
||||
} /* after */);
|
||||
} else {
|
||||
mBubbleContainer.cancelAllAnimations();
|
||||
mExpandedAnimationController.collapseBackToStack(
|
||||
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
|
||||
() -> {
|
||||
mBubbleContainer.setActiveController(mStackAnimationController);
|
||||
updateAfter.run();
|
||||
@@ -1110,6 +1108,10 @@ public class BubbleStackView extends FrameLayout {
|
||||
/** Called when a gesture is completed or cancelled. */
|
||||
void onGestureFinished() {
|
||||
mIsGestureInProgress = false;
|
||||
|
||||
if (mIsExpanded) {
|
||||
mExpandedAnimationController.onGestureFinished();
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepares and starts the desaturate/darken animation on the bubble stack. */
|
||||
@@ -1200,6 +1202,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
*/
|
||||
void magnetToStackIfNeededThenAnimateDismissal(
|
||||
View touchedView, float velX, float velY, Runnable after) {
|
||||
final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
|
||||
final Runnable animateDismissal = () -> {
|
||||
mAfterMagnet = null;
|
||||
|
||||
@@ -1217,7 +1220,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
resetDesaturationAndDarken();
|
||||
});
|
||||
} else {
|
||||
mExpandedAnimationController.dismissDraggedOutBubble(() -> {
|
||||
mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
|
||||
mAnimatingMagnet = false;
|
||||
mShowingDismiss = false;
|
||||
mDraggingInDismissTarget = false;
|
||||
@@ -1384,10 +1387,18 @@ public class BubbleStackView extends FrameLayout {
|
||||
};
|
||||
|
||||
// Post in case layout isn't complete and getWidth returns 0.
|
||||
post(() -> mFlyout.showFlyout(
|
||||
updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
|
||||
mStackAnimationController.isStackOnLeftSide(),
|
||||
bubble.iconView.getBadgeColor(), mAfterFlyoutHides));
|
||||
post(() -> {
|
||||
// An auto-expanding bubble could have been posted during the time it takes to
|
||||
// layout.
|
||||
if (isExpanded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mFlyout.showFlyout(
|
||||
updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
|
||||
mStackAnimationController.isStackOnLeftSide(),
|
||||
bubble.iconView.getBadgeColor(), mAfterFlyoutHides);
|
||||
});
|
||||
}
|
||||
|
||||
mFlyout.removeCallbacks(mHideFlyout);
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.graphics.PointF;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
@@ -67,6 +68,12 @@ public class ExpandedAnimationController
|
||||
/** Whether the dragged-out bubble is in the dismiss target. */
|
||||
private boolean mIndividualBubbleWithinDismissTarget = false;
|
||||
|
||||
private boolean mAnimatingExpand = false;
|
||||
private boolean mAnimatingCollapse = false;
|
||||
private Runnable mAfterExpand;
|
||||
private Runnable mAfterCollapse;
|
||||
private PointF mCollapsePoint;
|
||||
|
||||
/**
|
||||
* Whether the dragged out bubble is springing towards the touch point, rather than using the
|
||||
* default behavior of moving directly to the touch point.
|
||||
@@ -94,43 +101,61 @@ public class ExpandedAnimationController
|
||||
/** The bubble currently being dragged out of the row (to potentially be dismissed). */
|
||||
private View mBubbleDraggingOut;
|
||||
|
||||
/**
|
||||
* Drag velocities for the dragging-out bubble when the drag finished. These are used by
|
||||
* {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
|
||||
*/
|
||||
private float mBubbleDraggingOutVelX;
|
||||
private float mBubbleDraggingOutVelY;
|
||||
|
||||
/**
|
||||
* Animates expanding the bubbles into a row along the top of the screen.
|
||||
*/
|
||||
public void expandFromStack(PointF collapseTo, Runnable after) {
|
||||
animationsForChildrenFromIndex(
|
||||
0, /* startIndex */
|
||||
new ChildAnimationConfigurator() {
|
||||
@Override
|
||||
public void configureAnimationForChildAtIndex(
|
||||
int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) {
|
||||
animation.position(getBubbleLeft(index), getExpandedY());
|
||||
}
|
||||
})
|
||||
.startAll(after);
|
||||
public void expandFromStack(Runnable after) {
|
||||
mAnimatingCollapse = false;
|
||||
mAnimatingExpand = true;
|
||||
mAfterExpand = after;
|
||||
|
||||
mCollapseToPoint = collapseTo;
|
||||
startOrUpdateExpandAnimation();
|
||||
}
|
||||
|
||||
/** Animate collapsing the bubbles back to their stacked position. */
|
||||
public void collapseBackToStack(Runnable after) {
|
||||
// Stack to the left if we're going to the left, or right if not.
|
||||
final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;
|
||||
public void collapseBackToStack(PointF collapsePoint, Runnable after) {
|
||||
mAnimatingExpand = false;
|
||||
mAnimatingCollapse = true;
|
||||
mAfterCollapse = after;
|
||||
mCollapsePoint = collapsePoint;
|
||||
|
||||
startOrUpdateCollapseAnimation();
|
||||
}
|
||||
|
||||
private void startOrUpdateExpandAnimation() {
|
||||
animationsForChildrenFromIndex(
|
||||
0, /* startIndex */
|
||||
(index, animation) ->
|
||||
(index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
|
||||
.startAll(() -> {
|
||||
mAnimatingExpand = false;
|
||||
|
||||
if (mAfterExpand != null) {
|
||||
mAfterExpand.run();
|
||||
}
|
||||
|
||||
mAfterExpand = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void startOrUpdateCollapseAnimation() {
|
||||
// Stack to the left if we're going to the left, or right if not.
|
||||
final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
|
||||
animationsForChildrenFromIndex(
|
||||
0, /* startIndex */
|
||||
(index, animation) -> {
|
||||
animation.position(
|
||||
mCollapseToPoint.x + (sideMultiplier * index * mStackOffsetPx),
|
||||
mCollapseToPoint.y))
|
||||
.startAll(after /* endAction */);
|
||||
mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx),
|
||||
mCollapsePoint.y);
|
||||
})
|
||||
.startAll(() -> {
|
||||
mAnimatingCollapse = false;
|
||||
|
||||
if (mAfterCollapse != null) {
|
||||
mAfterCollapse.run();
|
||||
}
|
||||
|
||||
mAfterCollapse = null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Prepares the given bubble to be dragged out. */
|
||||
@@ -174,10 +199,10 @@ public class ExpandedAnimationController
|
||||
}
|
||||
|
||||
/** Plays a dismiss animation on the dragged out bubble. */
|
||||
public void dismissDraggedOutBubble(Runnable after) {
|
||||
public void dismissDraggedOutBubble(View bubble, Runnable after) {
|
||||
mIndividualBubbleWithinDismissTarget = false;
|
||||
|
||||
animationForChild(mBubbleDraggingOut)
|
||||
animationForChild(bubble)
|
||||
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
||||
.scaleX(1.1f)
|
||||
.scaleY(1.1f)
|
||||
@@ -187,6 +212,10 @@ public class ExpandedAnimationController
|
||||
updateBubblePositions();
|
||||
}
|
||||
|
||||
@Nullable public View getDraggedOutBubble() {
|
||||
return mBubbleDraggingOut;
|
||||
}
|
||||
|
||||
/** Magnets the given bubble to the dismiss target. */
|
||||
public void magnetBubbleToDismiss(
|
||||
View bubbleView, float velX, float velY, float destY, Runnable after) {
|
||||
@@ -225,24 +254,17 @@ public class ExpandedAnimationController
|
||||
final int index = mLayout.indexOfChild(bubbleView);
|
||||
|
||||
animationForChildAtIndex(index)
|
||||
.position(getBubbleLeft(index), getExpandedY())
|
||||
.withPositionStartVelocities(velX, velY)
|
||||
.start(() -> bubbleView.setTranslationZ(0f) /* after */);
|
||||
.position(getBubbleLeft(index), getExpandedY())
|
||||
.withPositionStartVelocities(velX, velY)
|
||||
.start(() -> bubbleView.setTranslationZ(0f) /* after */);
|
||||
|
||||
mBubbleDraggingOut = null;
|
||||
mBubbleDraggedOutEnough = false;
|
||||
updateBubblePositions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets configuration variables so that when the given bubble is removed, the animations are
|
||||
* started with the given velocities.
|
||||
*/
|
||||
public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
|
||||
mBubbleDraggingOut = bubbleView;
|
||||
mBubbleDraggingOutVelX = velX;
|
||||
mBubbleDraggingOutVelY = velY;
|
||||
/** Resets bubble drag out gesture flags. */
|
||||
public void onGestureFinished() {
|
||||
mBubbleDraggedOutEnough = false;
|
||||
mBubbleDraggingOut = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,14 +348,21 @@ public class ExpandedAnimationController
|
||||
|
||||
@Override
|
||||
void onChildAdded(View child, int index) {
|
||||
child.setTranslationX(getXForChildAtIndex(index));
|
||||
|
||||
animationForChild(child)
|
||||
.translationY(
|
||||
getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
|
||||
getExpandedY() /* to */)
|
||||
.start();
|
||||
updateBubblePositions();
|
||||
// If a bubble is added while the expand/collapse animations are playing, update the
|
||||
// animation to include the new bubble.
|
||||
if (mAnimatingExpand) {
|
||||
startOrUpdateExpandAnimation();
|
||||
} else if (mAnimatingCollapse) {
|
||||
startOrUpdateCollapseAnimation();
|
||||
} else {
|
||||
child.setTranslationX(getXForChildAtIndex(index));
|
||||
animationForChild(child)
|
||||
.translationY(
|
||||
getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
|
||||
getExpandedY() /* to */)
|
||||
.start();
|
||||
updateBubblePositions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -357,7 +386,16 @@ public class ExpandedAnimationController
|
||||
updateBubblePositions();
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {
|
||||
updateBubblePositions();
|
||||
}
|
||||
|
||||
private void updateBubblePositions() {
|
||||
if (mAnimatingExpand || mAnimatingCollapse) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
final View bubble = mLayout.getChildAt(i);
|
||||
|
||||
@@ -366,6 +404,7 @@ public class ExpandedAnimationController
|
||||
if (bubble.equals(mBubbleDraggingOut)) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationForChild(bubble)
|
||||
.translationX(getBubbleLeft(i))
|
||||
.start();
|
||||
|
||||
@@ -139,6 +139,9 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
*/
|
||||
abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
|
||||
|
||||
/** Called when a child view has been reordered in the view hierachy. */
|
||||
abstract void onChildReordered(View child, int oldIndex, int newIndex);
|
||||
|
||||
/**
|
||||
* Called when the controller is set as the active animation controller for the given
|
||||
* layout. Once active, the controller can start animations using the animator instances
|
||||
@@ -311,40 +314,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
super.addView(child, index, params);
|
||||
|
||||
// Set up animations for the new view, if the controller is set. If it isn't set, we'll be
|
||||
// setting up animations for all children when setActiveController is called.
|
||||
if (mController != null) {
|
||||
for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
|
||||
setUpAnimationForChild(property, child, index);
|
||||
}
|
||||
|
||||
mController.onChildAdded(child, index);
|
||||
}
|
||||
addViewInternal(child, index, params, false /* isReorder */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeView(View view) {
|
||||
removeViewAndThen(view, /* callback */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewAt(int index) {
|
||||
removeView(getChildAt(index));
|
||||
}
|
||||
|
||||
/** Immediately moves the view from wherever it currently is, to the given index. */
|
||||
public void moveViewTo(View view, int index) {
|
||||
super.removeView(view);
|
||||
addView(view, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Let the controller know that this view should be removed, and then call the callback once the
|
||||
* controller has finished any removal animations and the view has actually been removed.
|
||||
*/
|
||||
public void removeViewAndThen(View view, Runnable callback) {
|
||||
if (mController != null) {
|
||||
final int index = indexOfChild(view);
|
||||
|
||||
@@ -359,19 +333,28 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
// any are still running and then remove it.
|
||||
cancelAnimationsOnView(view);
|
||||
removeTransientView(view);
|
||||
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Without a controller, nobody will animate this view out, so it gets an unceremonious
|
||||
// departure.
|
||||
super.removeView(view);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
@Override
|
||||
public void removeViewAt(int index) {
|
||||
removeView(getChildAt(index));
|
||||
}
|
||||
|
||||
/** Immediately re-orders the view to the given index. */
|
||||
public void reorderView(View view, int index) {
|
||||
final int oldIndex = indexOfChild(view);
|
||||
|
||||
super.removeView(view);
|
||||
addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */);
|
||||
|
||||
if (mController != null) {
|
||||
mController.onChildReordered(view, oldIndex, index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +435,26 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a view to the layout. If this addition is not the result of a call to
|
||||
* {@link #reorderView}, this will also notify the controller via
|
||||
* {@link PhysicsAnimationController#onChildAdded} and set up animations for the view.
|
||||
*/
|
||||
private void addViewInternal(
|
||||
View child, int index, ViewGroup.LayoutParams params, boolean isReorder) {
|
||||
super.addView(child, index, params);
|
||||
|
||||
// Set up animations for the new view, if the controller is set. If it isn't set, we'll be
|
||||
// setting up animations for all children when setActiveController is called.
|
||||
if (mController != null && !isReorder) {
|
||||
for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
|
||||
setUpAnimationForChild(property, child, index);
|
||||
}
|
||||
|
||||
mController.onChildAdded(child, index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the animation of the given property from the view at the given index via the view
|
||||
* tag system.
|
||||
|
||||
@@ -650,6 +650,9 @@ public class StackAnimationController extends
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {}
|
||||
|
||||
@Override
|
||||
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
|
||||
Resources res = layout.getResources();
|
||||
|
||||
@@ -73,14 +73,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
@Test
|
||||
public void testExpansionAndCollapse() throws InterruptedException {
|
||||
Runnable afterExpand = Mockito.mock(Runnable.class);
|
||||
mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
|
||||
mExpandedController.expandFromStack(afterExpand);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
verify(afterExpand).run();
|
||||
|
||||
Runnable afterCollapse = Mockito.mock(Runnable.class);
|
||||
mExpandedController.collapseBackToStack(afterCollapse);
|
||||
mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
|
||||
@@ -139,7 +139,6 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
assertEquals(500f, draggedBubble.getTranslationY(), 1f);
|
||||
|
||||
// Snap it back and make sure it made it back correctly.
|
||||
mExpandedController.prepareForDismissalWithVelocity(draggedBubble, 0f, 0f);
|
||||
mLayout.removeView(draggedBubble);
|
||||
waitForLayoutMessageQueue();
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
@@ -169,7 +168,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
|
||||
// Dismiss the now-magneted bubble, verify that the callback was called.
|
||||
final Runnable afterDismiss = Mockito.mock(Runnable.class);
|
||||
mExpandedController.dismissDraggedOutBubble(afterDismiss);
|
||||
mExpandedController.dismissDraggedOutBubble(draggedOutView, afterDismiss);
|
||||
waitForPropertyAnimations(DynamicAnimation.ALPHA);
|
||||
verify(after).run();
|
||||
|
||||
@@ -224,7 +223,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
|
||||
/** Expand the stack and wait for animations to finish. */
|
||||
private void expand() throws InterruptedException {
|
||||
mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class));
|
||||
mExpandedController.expandFromStack(Mockito.mock(Runnable.class));
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -487,6 +486,9 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {}
|
||||
|
||||
@Override
|
||||
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
|
||||
}
|
||||
|
||||
@@ -274,6 +274,12 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
|
||||
() -> mWrappedController.onChildRemoved(child, index, finishRemoval));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {
|
||||
runOnMainThreadAndBlock(
|
||||
() -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
|
||||
runOnMainThreadAndBlock(
|
||||
|
||||
Reference in New Issue
Block a user