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());
-
}
}