Update bubble + flyout animations. - Brings the new bubble animation up to spec, where it scales into place and pushes the other bubbles to the side. - Updates the flyout entrance animation to expand from the dot, rather than simply sliding in. - When tapping the flyout, collapse first before expanding. Test: atest SystemUITests Fixes: 134512898 Change-Id: Ibc65221bf258010ef40943d88a88698964dee7cd
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java index 70cd2b7..e8f3058 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
@@ -39,8 +39,7 @@ import android.widget.FrameLayout; import android.widget.TextView; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.annotation.Nullable; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; @@ -67,9 +66,6 @@ private final ViewGroup mFlyoutTextContainer; private final TextView mFlyoutText; - /** Spring animation for the flyout. */ - private final SpringAnimation mFlyoutSpring = - new SpringAnimation(this, DynamicAnimation.TRANSLATION_X); /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */ private final float mNewDotRadius; @@ -141,7 +137,7 @@ private static final float DOT_SCALE = 0.8f; /** Callback to run when the flyout is hidden. */ - private Runnable mOnHide; + @Nullable private Runnable mOnHide; public BubbleFlyoutView(Context context) { super(context); @@ -209,17 +205,16 @@ super.onDraw(canvas); } - /** Configures the flyout and animates it in. */ - void showFlyout( + /** Configures the flyout, collapsed into to dot form. */ + void setupFlyoutStartingAsDot( CharSequence updateMessage, PointF stackPos, float parentWidth, - boolean arrowPointingLeft, int dotColor, Runnable onHide) { + boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete, + @Nullable Runnable onHide) { mArrowPointingLeft = arrowPointingLeft; mDotColor = dotColor; mOnHide = onHide; - setCollapsePercent(0f); - setAlpha(0f); - setVisibility(VISIBLE); + setCollapsePercent(1f); // Set the flyout TextView's max width in terms of percent, and then subtract out the // padding so that the entire flyout view will be the desired width (rather than the @@ -245,14 +240,6 @@ ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble : stackPos.x - getWidth() - mFlyoutSpaceFromBubble; - // Translate towards the stack slightly. - setTranslationX( - mRestingTranslationX + (arrowPointingLeft ? -mBubbleSize : mBubbleSize)); - - // Fade in the entire flyout and spring it to its normal position. - animate().alpha(1f); - mFlyoutSpring.animateToFinalPosition(mRestingTranslationX); - // Calculate the difference in size between the flyout and the 'dot' so that we can // transform into the dot later. mFlyoutToDotWidthDelta = getWidth() - mNewDotSize; @@ -271,12 +258,17 @@ getHeight() / 2f - mBubbleSize / 2f + mOriginalDotSize / 2; + + if (onLayoutComplete != null) { + onLayoutComplete.run(); + } }); } /** - * Hides the flyout and runs the optional callback passed into showFlyout. The flyout has been - * animated into the 'new' dot by the time we call this, so no animations are needed. + * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot. + * The flyout has been animated into the 'new' dot by the time we call this, so no animations + * are needed. */ void hideFlyout() { if (mOnHide != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 10fac81..abd4229 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -288,6 +288,11 @@ private float mFlyoutDragDeltaX = 0f; /** + * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. + */ + private Runnable mAnimateInFlyout; + + /** * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides * it immediately. */ @@ -370,7 +375,7 @@ addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); mFlyoutTransitionSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setStiffness(SpringForce.STIFFNESS_LOW) .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); @@ -1092,6 +1097,19 @@ } /** + * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout + * once it collapses. + */ + @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null; + + void onFlyoutTapped() { + mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); + + mFlyout.removeCallbacks(mHideFlyout); + mHideFlyout.run(); + } + + /** * Called when the flyout drag has finished, and returns true if the gesture successfully * dismissed the flyout. */ @@ -1288,6 +1306,12 @@ /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ private void animateFlyoutCollapsed(boolean collapsed, float velX) { final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's + // faster. + mFlyoutTransitionSpring.getSpring().setStiffness( + (mBubbleToExpandAfterFlyoutCollapse != null) + ? SpringForce.STIFFNESS_MEDIUM + : SpringForce.STIFFNESS_LOW); mFlyoutTransitionSpring .setStartValue(mFlyoutDragDeltaX) .setStartVelocity(velX) @@ -1329,8 +1353,10 @@ if (updateMessage == null || isExpanded() || mIsExpansionAnimating - || mIsGestureInProgress) { - // Skip the message if none exists, we're expanded or animating expansion. + || mIsGestureInProgress + || mBubbleToExpandAfterFlyoutCollapse != null) { + // Skip the message if none exists, we're expanded or animating expansion, or we're + // about to expand a bubble from the previous tapped flyout. return; } @@ -1339,18 +1365,14 @@ bubble.getIconView().setSuppressDot( true /* suppressDot */, false /* animate */); + mFlyout.removeCallbacks(mAnimateInFlyout); mFlyoutDragDeltaX = 0f; - mFlyout.setAlpha(0f); if (mAfterFlyoutHides != null) { mAfterFlyoutHides.run(); } mAfterFlyoutHides = () -> { - if (bubble.getIconView() == null) { - return; - } - final boolean suppressDot = !bubble.showBubbleDot(); // If we're going to suppress the dot, make it visible first so it'll // visibly animate away. @@ -1365,8 +1387,16 @@ bubble.getIconView().setSuppressDot( suppressDot /* suppressDot */, suppressDot /* animate */); + + if (mBubbleToExpandAfterFlyoutCollapse != null) { + mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); + mBubbleData.setExpanded(true); + mBubbleToExpandAfterFlyoutCollapse = null; + } }; + mFlyout.setVisibility(INVISIBLE); + // Post in case layout isn't complete and getWidth returns 0. post(() -> { // An auto-expanding bubble could have been posted during the time it takes to @@ -1375,10 +1405,29 @@ return; } - mFlyout.showFlyout( + final Runnable afterShow = () -> { + mAnimateInFlyout = () -> { + mFlyout.setVisibility(VISIBLE); + bubble.getIconView().setSuppressDot( + true /* suppressDot */, false /* animate */); + mFlyoutDragDeltaX = + mStackAnimationController.isStackOnLeftSide() + ? -mFlyout.getWidth() + : mFlyout.getWidth(); + animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + }; + + mFlyout.postDelayed(mAnimateInFlyout, 200); + }; + + mFlyout.setupFlyoutStartingAsDot( updateMessage, mStackAnimationController.getStackPosition(), getWidth(), mStackAnimationController.isStackOnLeftSide(), - bubble.getIconView().getBadgeColor(), mAfterFlyoutHides); + bubble.getIconView().getBadgeColor(), + afterShow, + mAfterFlyoutHides); + mFlyout.bringToFront(); }); } @@ -1393,6 +1442,7 @@ mAfterFlyoutHides.run(); } + mFlyout.removeCallbacks(mAnimateInFlyout); mFlyout.removeCallbacks(mHideFlyout); mFlyout.hideFlyout(); }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index 21bb35d..4c5b3fe 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -192,9 +192,8 @@ } }); } else if (isFlyout) { - // TODO(b/129768381): Expand if tapped, dismiss if swiped away. if (!mBubbleData.isExpanded() && !mMovedEnough) { - mBubbleData.setExpanded(true); + mStack.onFlyoutTapped(); } } else if (mMovedEnough) { if (isStack) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index 4ddb255..9821ecb 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -56,6 +56,10 @@ /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */ private static final int ANIMATE_TRANSLATION_FACTOR = 4; + /** Values to use for animating bubbles in. */ + private static final float ANIMATE_IN_STIFFNESS = 1000f; + private static final int ANIMATE_IN_START_DELAY = 25; + /** * Values to use for the default {@link SpringForce} provided to the physics animation layout. */ @@ -643,7 +647,7 @@ } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble // to the back of the stack, it'll be largely invisible so don't bother animating it in. - animateInBubble(child); + animateInBubble(child, index); } } @@ -697,7 +701,7 @@ // Animate in the top bubble now that we're visible. if (mLayout.getChildCount() > 0) { - animateInBubble(mLayout.getChildAt(0)); + animateInBubble(mLayout.getChildAt(0), 0 /* index */); } }); } @@ -760,21 +764,34 @@ } /** Animates in the given bubble. */ - private void animateInBubble(View child) { + private void animateInBubble(View child, int index) { if (!isActiveController()) { return; } - child.setTranslationY(mStackPosition.y); + final float xOffset = + getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); - float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); + // Position the new bubble in the correct position, scaled down completely. + child.setTranslationX(mStackPosition.x + xOffset * index); + child.setTranslationY(mStackPosition.y); + child.setScaleX(0f); + child.setScaleY(0f); + + // Push the subsequent views out of the way, if there are subsequent views. + if (index + 1 < mLayout.getChildCount()) { + animationForChildAtIndex(index + 1) + .translationX(mStackPosition.x + xOffset * (index + 1)) + .withStiffness(SpringForce.STIFFNESS_LOW) + .start(); + } + + // Scale in the new bubble, slightly delayed. animationForChild(child) - .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) - .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) - .alpha(0f /* from */, 1f /* to */) - .translationX( - mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */, - mStackPosition.x /* to */) + .scaleX(1f) + .scaleY(1f) + .withStiffness(ANIMATE_IN_STIFFNESS) + .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0) .start(); }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java index 173237f..bd63e76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
@@ -18,7 +18,6 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; -import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.verify; @@ -57,16 +56,19 @@ @Test public void testShowFlyout_isVisible() { - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null); + mFlyout.setVisibility(View.VISIBLE); + assertEquals("Hello", mFlyoutText.getText()); assertEquals(View.VISIBLE, mFlyout.getVisibility()); - assertEquals(1f, mFlyoutText.getAlpha(), .01f); } @Test public void testFlyoutHide_runsCallback() { Runnable after = Mockito.mock(Runnable.class); - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, after); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after); mFlyout.hideFlyout(); verify(after).run(); @@ -74,19 +76,16 @@ @Test public void testSetCollapsePercent() { - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); - - float initialTranslationZ = mFlyout.getTranslationZ(); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null); + mFlyout.setVisibility(View.VISIBLE); mFlyout.setCollapsePercent(1f); assertEquals(0f, mFlyoutText.getAlpha(), 0.01f); assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse. - assertTrue(mFlyout.getTranslationZ() < initialTranslationZ); // Should be descending. mFlyout.setCollapsePercent(0f); assertEquals(1f, mFlyoutText.getAlpha(), 0.01f); assertEquals(0f, mFlyoutText.getTranslationX()); - assertEquals(initialTranslationZ, mFlyout.getTranslationZ()); - } }