Animate addition/removal of views in expanded mode.

This required adding the setChildVisibility method to controllers, to allow them to animate in/out views that pass the max rendered child threshold. This was not previously relevant since in the bubble stack, you can't really see the views when they're set to VISIBLE/GONE.

Also, renamed onChildToBeRemoved to onChildRemoved since that's more accurate given the move to transient views.

Test: atest SystemUITests
Change-Id: I291ff8f6257ba54e0688c1062bbd673e0c7bdb5c
diff --git a/packages/SystemUI/docs/physics-animation-layout.md b/packages/SystemUI/docs/physics-animation-layout.md
index a67b5e8..300f63a 100644
--- a/packages/SystemUI/docs/physics-animation-layout.md
+++ b/packages/SystemUI/docs/physics-animation-layout.md
@@ -26,7 +26,7 @@
 
 ### Animation Control Methods
 ![Diagram of how calls to animateValueForChildAtIndex dispatch to DynamicAnimations.](physics-animation-layout-control-methods.png)
-Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded``` and ```onChildRemoved``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views.
+Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded```, ```onChildRemoved```, and ```setChildVisibility``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out/visible/gone. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views.
 
 In either case, the controller has access to the layout’s protected ```animateValueForChildAtIndex(ViewProperty property, int index, float value)``` method. This method is used to actually run an animation.
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 4f870f6..1644064 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -38,6 +38,12 @@
         extends PhysicsAnimationLayout.PhysicsAnimationController {
 
     /**
+     * How much to translate the bubbles when they're animating in/out. This value is multiplied by
+     * the bubble size.
+     */
+    private static final int ANIMATE_TRANSLATION_FACTOR = 4;
+
+    /**
      * The stack position from which the bubbles were expanded. Saved in {@link #expandFromStack}
      * and used to return to stack form in {@link #collapseBackToStack}.
      */
@@ -125,7 +131,10 @@
     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
         return Sets.newHashSet(
                 DynamicAnimation.TRANSLATION_X,
-                DynamicAnimation.TRANSLATION_Y);
+                DynamicAnimation.TRANSLATION_Y,
+                DynamicAnimation.SCALE_X,
+                DynamicAnimation.SCALE_Y,
+                DynamicAnimation.ALPHA);
     }
 
     @Override
@@ -147,13 +156,55 @@
 
     @Override
     void onChildAdded(View child, int index) {
-        // TODO: Animate the new bubble into the row, and push the other bubbles out of the way.
-        child.setTranslationY(getExpandedY());
+        // Pop in from the top.
+        // TODO: Reverse this when bubbles are at the bottom.
+        child.setTranslationX(getXForChildAtIndex(index));
+        child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
+        mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY());
+
+        // Animate the remaining bubbles to the correct X position.
+        for (int i = index + 1; i < mLayout.getChildCount(); i++) {
+            mLayout.animateValueForChildAtIndex(
+                    DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
+        }
     }
 
     @Override
-    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
-        // TODO: Animate the bubble out, and pull the other bubbles into its position.
-        actuallyRemove.run();
+    void onChildRemoved(View child, int index, Runnable finishRemoval) {
+        // Bubble pops out to the top.
+        // TODO: Reverse this when bubbles are at the bottom.
+        mLayout.animateValueForChild(
+                DynamicAnimation.ALPHA, child, 0f, finishRemoval);
+        mLayout.animateValueForChild(
+                DynamicAnimation.TRANSLATION_Y,
+                child,
+                getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
+
+        // Animate the remaining bubbles to the correct X position.
+        for (int i = index; i < mLayout.getChildCount(); i++) {
+            mLayout.animateValueForChildAtIndex(
+                    DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
+        }
+    }
+
+    @Override
+    protected void setChildVisibility(View child, int index, int visibility) {
+        if (visibility == View.VISIBLE) {
+            // Set alpha to 0 but then become visible immediately so the animation is visible.
+            child.setAlpha(0f);
+            child.setVisibility(View.VISIBLE);
+        }
+
+        // Fade in.
+        mLayout.animateValueForChild(
+                DynamicAnimation.ALPHA,
+                child,
+                /* value */ visibility == View.GONE ? 0f : 1f,
+                () -> super.setChildVisibility(child, index, visibility));
+    }
+
+    /** Returns the appropriate X translation value for a bubble at the given index. */
+    private float getXForChildAtIndex(int index) {
+        return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
index e4e6bc9..a4ddbf7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -92,14 +92,18 @@
         abstract void onChildAdded(View child, int index);
 
         /**
-         * Called when a child is to be removed from the layout. Controllers can use this
-         * opportunity to animate out the new view before calling the provided callback to actually
-         * remove it.
+         * Called with a child view that has been removed from the layout, from the given index. The
+         * passed view has been removed from the layout and added back as a transient view, which
+         * renders normally, but is not part of the normal view hierarchy and will not be considered
+         * by getChildAt() and getChildCount().
          *
-         * Controllers should be careful to ensure that actuallyRemove is called on all code paths
-         * or child views will never be removed.
+         * The controller can perform animations on the child (either manually, or by using
+         * {@link #animateValueForChild}), and then call finishRemoval when complete.
+         *
+         * finishRemoval must be called by implementations of this method, or transient views will
+         * never be removed.
          */
-        abstract void onChildToBeRemoved(View child, int index, Runnable actuallyRemove);
+        abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
 
         protected PhysicsAnimationLayout mLayout;
 
@@ -112,6 +116,15 @@
         protected PhysicsAnimationLayout getLayout() {
             return mLayout;
         }
+
+        /**
+         * Sets the child's visibility when it moves beyond or within the limits set by a call to
+         * {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate
+         * this transition.
+         */
+        protected void setChildVisibility(View child, int index, int visibility) {
+            child.setVisibility(visibility);
+        }
     }
 
     /**
@@ -236,7 +249,7 @@
 
             // Tell the controller to animate this view out, and call the callback when it's
             // finished.
-            mController.onChildToBeRemoved(view, index, () -> {
+            mController.onChildRemoved(view, index, () -> {
                 // Done animating, remove the transient view.
                 removeTransientView(view);
 
@@ -457,11 +470,16 @@
     /** Hides children beyond the max rendering count. */
     private void setChildrenVisibility() {
         for (int i = 0; i < getChildCount(); i++) {
-            getChildAt(i).setVisibility(
-                    // Ignore views that are animating out when calculating whether to hide the
-                    // view. That is, if we're supposed to render 5 views, but 4 are animating out
-                    // and will soon be removed, render up to 9 views temporarily.
-                    i < mMaxRenderedChildren ? View.VISIBLE : View.GONE);
+            final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE;
+            final View targetView = getChildAt(i);
+
+            if (targetView.getVisibility() != targetVisibility) {
+                if (mController != null) {
+                    mController.setChildVisibility(targetView, i, targetVisibility);
+                } else {
+                    targetView.setVisibility(targetVisibility);
+                }
+            }
         }
     }
 
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 23c6fd3..0c089a75 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -316,10 +316,10 @@
     }
 
     @Override
-    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
+    void onChildRemoved(View child, int index, Runnable finishRemoval) {
         // Animate the child out, actually removing it once its alpha is zero.
         mLayout.animateValueForChild(
-                DynamicAnimation.ALPHA, child, 0f, actuallyRemove);
+                DynamicAnimation.ALPHA, child, 0f, finishRemoval);
         mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_IN_STARTING_SCALE);
         mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_IN_STARTING_SCALE);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
index 1bb7ef4..c0aac7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -55,11 +55,12 @@
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
         mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+
+        mExpansionPoint = new PointF(100, 100);
     }
 
     @Test
     public void testExpansionAndCollapse() throws InterruptedException {
-        mExpansionPoint = new PointF(100, 100);
         Runnable afterExpand = Mockito.mock(Runnable.class);
         mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
 
@@ -77,27 +78,48 @@
         Mockito.verify(afterExpand).run();
     }
 
+    @Test
+    public void testOnChildRemoved() throws InterruptedException {
+        Runnable afterExpand = Mockito.mock(Runnable.class);
+        mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        testExpanded();
+
+        // Remove some views and see if the remaining child views still pass the expansion test.
+        mLayout.removeView(mViews.get(0));
+        mLayout.removeView(mViews.get(3));
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        testExpanded();
+    }
+
     /** Check that children are in the correct positions for being stacked. */
     private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
         // Make sure the rest of the stack moved again, including the first bubble not moving, and
         // is stacked to the right now that we're on the right side of the screen.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(x + i * offsetMultiplier * mStackOffset,
-                    mViews.get(i).getTranslationX(), 2f);
-            assertEquals(y, mViews.get(i).getTranslationY(), 2f);
+                    mLayout.getChildAt(i).getTranslationX(), 2f);
+            assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
+
+            if (i < mMaxRenderedBubbles) {
+                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
+            }
         }
     }
 
     /** Check that children are in the correct positions for being expanded. */
     private void testExpanded() {
-        // Make sure the rest of the stack moved again, including the first bubble not moving, and
-        // is stacked to the right now that we're on the right side of the screen.
-        for (int i = 0; i < mLayout.getChildCount(); i++) {
+        // Check all the visible bubbles to see if they're in the right place.
+        for (int i = 0; i < Math.min(mLayout.getChildCount(), mMaxRenderedBubbles); i++) {
             assertEquals(mBubblePadding + (i * (mBubbleSize + mBubblePadding)),
-                    mViews.get(i).getTranslationX(),
+                    mLayout.getChildAt(i).getTranslationX(),
                     2f);
             assertEquals(mBubblePadding + mCutoutInsetSize,
-                    mViews.get(i).getTranslationY(), 2f);
+                    mLayout.getChildAt(i).getTranslationY(), 2f);
+
+            if (i < mMaxRenderedBubbles) {
+                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
+            }
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
index 5be991f..c3214040 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java
@@ -25,6 +25,7 @@
 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 android.os.SystemClock;
 import android.support.test.filters.SmallTest;
@@ -100,9 +101,9 @@
         mTestableController.setRemoveImmediately(true);
         mLayout.removeView(mViews.get(1));
         mLayout.removeView(mViews.get(2));
-        Mockito.verify(mTestableController).onChildToBeRemoved(
+        Mockito.verify(mTestableController).onChildRemoved(
                 eq(mViews.get(1)), eq(1), any());
-        Mockito.verify(mTestableController).onChildToBeRemoved(
+        Mockito.verify(mTestableController).onChildRemoved(
                 eq(mViews.get(2)), eq(1), any());
 
         // Make sure we still get view added notifications after doing some removals.
@@ -345,6 +346,24 @@
         assertTrue(mViews.get(0).getTranslationY() < 1000);
     }
 
+    @Test
+    public void testSetChildVisibility() throws InterruptedException {
+        mLayout.setController(mTestableController);
+        addOneMoreThanRenderLimitBubbles();
+
+        // The last view should have been set to GONE by the controller, since we added one more
+        // than the limit and it got pushed off. None of the first children should have been set
+        // VISIBLE, since they would have been animated in by onChildAdded.
+        Mockito.verify(mTestableController).setChildVisibility(
+                mViews.get(mViews.size() - 1), 5, View.GONE);
+        Mockito.verify(mTestableController, never()).setChildVisibility(
+                any(View.class), anyInt(), eq(View.VISIBLE));
+
+        // Remove the first view, which should cause the last view to become visible again.
+        mLayout.removeView(mViews.get(0));
+        Mockito.verify(mTestableController).setChildVisibility(
+                mViews.get(mViews.size() - 1), 4, View.VISIBLE);
+    }
 
     /** Standard test of chained translation animations. */
     private void testChainedTranslationAnimations() throws InterruptedException {
@@ -440,10 +459,15 @@
         void onChildAdded(View child, int index) {}
 
         @Override
-        void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
+        void onChildRemoved(View child, int index, Runnable finishRemoval) {
             if (mRemoveImmediately) {
-                actuallyRemove.run();
+                finishRemoval.run();
             }
         }
+
+        @Override
+        protected void setChildVisibility(View child, int index, int visibility) {
+            super.setChildVisibility(child, index, visibility);
+        }
     }
 }