Animate out bubbles in reverse of the way they animate in.

Also, fixes a bug where the last bubble doesn't animate out because the stack view resizes before the animation completes.

Test: atest SystemUITests
Fixes: 134514227
Change-Id: Ic36b79d96430307e58214bc968e0d4a16e7efa77
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 7d9bb07..494c1a2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -802,16 +802,32 @@
         if (mStackView == null) {
             return;
         }
+
         if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) {
             // Bubbles only appear in unlocked shade
             mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE);
-        } else if (mStackView != null) {
+        } else if (!hasBubbles() && mStackView.hasTransientBubbles()) {
+            // If we only have transient bubbles, then wait until they're gone.
+            mStackView.runActionAfterTransientViewAnimations(() -> {
+                mStackView.setVisibility(INVISIBLE);
+                updateBubblesShowing();
+            });
+        } else {
             mStackView.setVisibility(INVISIBLE);
         }
 
-        // Let listeners know if bubble state changed.
+        updateBubblesShowing();
+    }
+
+    /**
+     * Updates the status bar window controller and the state change listener with whether bubbles
+     * are currently showing.
+     */
+    public void updateBubblesShowing() {
         boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
-        boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
+        boolean hasBubblesShowing =
+                (hasBubbles() || mStackView.hasTransientBubbles())
+                        && mStackView.getVisibility() == VISIBLE;
         mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
         if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
             mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 7379693..ecf3191 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -657,6 +657,10 @@
         return mIsExpanded;
     }
 
+    void runActionAfterTransientViewAnimations(Runnable after) {
+        mStackAnimationController.runActionAfterAllViewsAndTransientRemoved(after);
+    }
+
     /**
      * The {@link BubbleView} that is expanded, null if one does not exist.
      */
@@ -687,6 +691,10 @@
         }
     }
 
+    boolean hasTransientBubbles() {
+        return mBubbleContainer.getTransientViewCount() > 0;
+    }
+
     // via BubbleData.Listener
     void addBubble(Bubble bubble) {
         if (DEBUG_BUBBLE_STACK_VIEW) {
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 94d4f59..6ec1e46 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -144,6 +144,11 @@
      */
     private boolean mFirstBubbleSpringingToTouch = false;
 
+    /**
+     * Action to run after all views, including transient views, have been removed from the layout.
+     */
+    @Nullable private Runnable mAfterAllViewsAndTransientRemoved = null;
+
     /** Horizontal offset of bubbles in the stack. */
     private float mStackOffset;
     /** Diameter of the bubble icon. */
@@ -561,6 +566,14 @@
     }
 
     /**
+     * Sets an action to run after all views, including transient views, have been removed from the
+     * layout.
+     */
+    public void runActionAfterAllViewsAndTransientRemoved(Runnable action) {
+        mAfterAllViewsAndTransientRemoved = action;
+    }
+
+    /**
      * Springs the first bubble to the given final position, with the rest of the stack 'following'.
      */
     protected void springFirstBubbleWithStackFollowing(
@@ -647,13 +660,26 @@
 
     @Override
     void onChildRemoved(View child, int index, Runnable finishRemoval) {
-        // Animate the removing view in the opposite direction of the stack.
-        final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
         animationForChild(child)
-                .alpha(0f, finishRemoval /* after */)
-                .scaleX(ANIMATE_IN_STARTING_SCALE)
-                .scaleY(ANIMATE_IN_STARTING_SCALE)
-                .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
+                .alpha(0f,
+                        finishRemoval /* after */,
+                        () -> {
+                            // If this was the last transient view, run the callback.
+                            if (mLayout.getTransientViewCount() == 0
+                                    && mAfterAllViewsAndTransientRemoved != null) {
+
+                                // If a 'real' view was added while we were animating out, don't run
+                                // the callback since all views haven't been removed.
+                                if (mLayout.getChildCount() == 0) {
+                                    mAfterAllViewsAndTransientRemoved.run();
+                                }
+
+                                mAfterAllViewsAndTransientRemoved = null;
+                            }
+                        } /* after */)
+                .scaleX(0f)
+                .scaleY(0f)
+                .withStiffness(ANIMATE_IN_STIFFNESS)
                 .start();
 
         if (mLayout.getChildCount() > 0) {