resolve merge conflicts of 65cba0fa324f610923b4b88709ebdb9128ec5dcd to qt-r1-bubbles-dev

Bug: None
Test: atest SystemUITests
Change-Id: I7194279a6a273cc97353857dadf73d413c7c9580
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 560e264..40fb6d2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -330,9 +330,7 @@
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
         mBubbleContainer = new PhysicsAnimationLayout(context);
-        mBubbleContainer.setMaxRenderedChildren(
-                getResources().getInteger(R.integer.bubbles_max_rendered));
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         mBubbleContainer.setElevation(elevation);
         mBubbleContainer.setClipChildren(false);
         addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -728,7 +726,7 @@
     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);
         }
     }
 
@@ -905,8 +903,10 @@
 
         mBubbleContainer.cancelAllAnimations();
         mExpandedAnimationController.collapseBackToStack(
+                mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
+                /* collapseTo */,
                 () -> {
-                    mBubbleContainer.setController(mStackAnimationController);
+                    mBubbleContainer.setActiveController(mStackAnimationController);
                     afterExpandedViewAnimation();
                 });
 
@@ -921,14 +921,11 @@
         mIsExpanded = true;
         beforeExpandedViewAnimation();
 
-        mBubbleContainer.setController(mExpandedAnimationController);
-        mExpandedAnimationController.expandFromStack(
-                mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
-                /* collapseTo */,
-                () -> {
-                    updatePointerPosition();
-                    afterExpandedViewAnimation();
-                } /* after */);
+        mBubbleContainer.setActiveController(mExpandedAnimationController);
+        mExpandedAnimationController.expandFromStack(() -> {
+            updatePointerPosition();
+            afterExpandedViewAnimation();
+        } /* after */);
 
 
         mExpandedViewContainer.setTranslationX(getCollapsedX());
@@ -1017,7 +1014,7 @@
         }
 
         mStackAnimationController.cancelStackPositionAnimations();
-        mBubbleContainer.setController(mStackAnimationController);
+        mBubbleContainer.setActiveController(mStackAnimationController);
         hideFlyoutImmediate();
 
         mDraggingInDismissTarget = false;
@@ -1115,6 +1112,10 @@
     /** 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. */
@@ -1205,6 +1206,7 @@
      */
     void magnetToStackIfNeededThenAnimateDismissal(
             View touchedView, float velX, float velY, Runnable after) {
+        final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
         final Runnable animateDismissal = () -> {
             mAfterMagnet = null;
 
@@ -1222,7 +1224,7 @@
                             resetDesaturationAndDarken();
                         });
             } else {
-                mExpandedAnimationController.dismissDraggedOutBubble(() -> {
+                mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
                     mAnimatingMagnet = false;
                     mShowingDismiss = false;
                     mDraggingInDismissTarget = false;
@@ -1389,10 +1391,18 @@
                 };
 
                 // 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);
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 c5fe91c..f111f04 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -22,6 +22,7 @@
 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 @@
     /** 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,49 +101,61 @@
     /** The bubble currently being dragged out of the row (to potentially be dismissed). */
     private View mBubbleDraggingOut;
 
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        final Resources res = layout.getResources();
-        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
-        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-        mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
-    }
-
     /**
      * 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. */
@@ -180,10 +199,10 @@
     }
 
     /** 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)
@@ -193,6 +212,10 @@
         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) {
@@ -231,21 +254,18 @@
         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.
-     */
-    public void prepareForDismissalWithVelocity(View bubbleView) {
-        mBubbleDraggingOut = bubbleView;
+    /** Resets bubble drag out gesture flags. */
+    public void onGestureFinished() {
         mBubbleDraggedOutEnough = false;
+        mBubbleDraggingOut = null;
+        updateBubblePositions();
     }
 
     /**
@@ -271,6 +291,23 @@
     }
 
     @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        final Resources res = layout.getResources();
+        mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
+        mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+        mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
+
+        // Ensure that all child views are at 1x scale, and visible, in case they were animating
+        // in.
+        mLayout.setVisibility(View.VISIBLE);
+        animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
+                animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
+    }
+
+    @Override
     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
         return Sets.newHashSet(
                 DynamicAnimation.TRANSLATION_X,
@@ -299,12 +336,21 @@
 
     @Override
     void onChildAdded(View child, int 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(getBubbleLeft(index));
+            animationForChild(child)
+                    .translationY(
+                            getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
+                            getExpandedY() /* to */)
+                    .start();
+            updateBubblePositions();
+        }
     }
 
     @Override
@@ -329,19 +375,15 @@
     }
 
     @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);
-        }
-
-        animationForChild(child)
-                .alpha(visibility == View.GONE ? 0f : 1f)
-                .start(() -> super.setChildVisibility(child, index, visibility) /* after */);
+    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);
 
@@ -350,6 +392,7 @@
             if (bubble.equals(mBubbleDraggingOut)) {
                 return;
             }
+
             animationForChild(bubble)
                     .translationX(getBubbleLeft(i))
                     .start();
@@ -371,10 +414,6 @@
         }
 
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mBubblesMaxRendered) {
-            // Only rendered bubbles are relevant for calculating row left.
-            bubbleCount = mBubblesMaxRendered;
-        }
 
         final float totalBubbleWidth = bubbleCount * mBubbleSizePx;
         final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
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 997d2c4..3a33392 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -17,10 +17,12 @@
 package com.android.systemui.bubbles.animation;
 
 import android.content.Context;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
@@ -137,12 +139,33 @@
          */
         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
+         * returned by {@link #animationForChild}.
+         *
+         * While all animations started by the previous controller will be cancelled, the new
+         * controller should not make any assumptions about the state of the layout or its children.
+         * Their translation, alpha, scale, etc. values may have been changed by the previous
+         * controller and should be reset here if relevant.
+         */
+        abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout);
+
         protected PhysicsAnimationLayout mLayout;
 
         PhysicsAnimationController() { }
 
+        /** Whether this controller is the currently active controller for its associated layout. */
+        protected boolean isActiveController() {
+            return this == mLayout.mController;
+        }
+
         protected void setLayout(PhysicsAnimationLayout layout) {
             this.mLayout = layout;
+            onActiveControllerForLayout(layout);
         }
 
         protected PhysicsAnimationLayout getLayout() {
@@ -150,15 +173,6 @@
         }
 
         /**
-         * 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);
-        }
-
-        /**
          * Returns a {@link PhysicsPropertyAnimator} instance for the given child view.
          */
         protected PhysicsPropertyAnimator animationForChild(View child) {
@@ -170,6 +184,9 @@
                 child.setTag(R.id.physics_animator_tag, animator);
             }
 
+            animator.clearAnimator();
+            animator.setAssociatedController(this);
+
             return animator;
         }
 
@@ -235,32 +252,17 @@
             new HashMap<>();
 
     /** The currently active animation controller. */
-    private PhysicsAnimationController mController;
-
-    /**
-     * The maximum number of children to render and animate at a time. See
-     * {@link #setMaxRenderedChildren}.
-     */
-    private int mMaxRenderedChildren = 5;
+    @Nullable protected PhysicsAnimationController mController;
 
     public PhysicsAnimationLayout(Context context) {
         super(context);
     }
 
     /**
-     * The maximum number of children to render and animate at a time. Any child views added beyond
-     * this limit will be set to {@link View#GONE}. If any animations attempt to run on the view,
-     * the corresponding property will be set with no animation.
-     */
-    public void setMaxRenderedChildren(int max) {
-        this.mMaxRenderedChildren = max;
-    }
-
-    /**
      * Sets the animation controller and constructs or reconfigures the layout's physics animations
      * to meet the controller's specifications.
      */
-    public void setController(PhysicsAnimationController controller) {
+    public void setActiveController(PhysicsAnimationController controller) {
         cancelAllAnimations();
         mEndActionForProperty.clear();
 
@@ -312,42 +314,11 @@
 
     @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 setController is called.
-        if (mController != null) {
-            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
-                setUpAnimationForChild(property, child, index);
-            }
-
-            mController.onChildAdded(child, index);
-        }
-
-        setChildrenVisibility();
+        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);
 
@@ -355,8 +326,6 @@
             super.removeView(view);
             addTransientView(view, index);
 
-            setChildrenVisibility();
-
             // Tell the controller to animate this view out, and call the callback when it's
             // finished.
             mController.onChildRemoved(view, index, () -> {
@@ -364,19 +333,28 @@
                 // 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);
         }
     }
 
@@ -427,6 +405,10 @@
         }
     }
 
+    protected boolean isActiveController(PhysicsAnimationController controller) {
+        return mController == controller;
+    }
+
     /** Whether the first child would be left of center if translated to the given x value. */
     protected boolean isFirstChildXLeftOfCenter(float x) {
         if (getChildCount() > 0) {
@@ -454,6 +436,26 @@
     }
 
     /**
+     * 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.
      */
@@ -481,33 +483,16 @@
         SpringAnimation newAnim = new SpringAnimation(child, property);
         newAnim.addUpdateListener((animation, value, velocity) -> {
             final int indexOfChild = indexOfChild(child);
-            final int nextAnimInChain =
-                    mController.getNextAnimationInChain(property, indexOfChild);
+            final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild);
 
             if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) {
                 return;
             }
 
-            final int animIndex = indexOfChild(child);
-            final float offset =
-                    mController.getOffsetForChainedPropertyAnimation(property);
-
-            // If this property's animations should be chained, then check to see if there is a
-            // subsequent animation within the rendering limit, and if so, tell it to animate to
-            // this animation's new value (plus the offset).
-            if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) {
-                getAnimationAtIndex(property, animIndex + 1)
+            final float offset = mController.getOffsetForChainedPropertyAnimation(property);
+            if (nextAnimInChain < getChildCount()) {
+                getAnimationAtIndex(property, nextAnimInChain)
                         .animateToFinalPosition(value + offset);
-            } else if (nextAnimInChain < getChildCount()) {
-                // If the next child view is not rendered, update the property directly without
-                // animating it, so that the view is still in the correct state if it later
-                // becomes visible.
-                for (int i = nextAnimInChain; i < getChildCount(); i++) {
-                    // 'value' here is the value of the last child within the rendering limit,
-                    // not the first child's value - so we want to subtract the last child's
-                    // index when calculating the offset.
-                    property.setValue(getChildAt(i), value + offset * (i - animIndex));
-                }
             }
         });
 
@@ -516,22 +501,6 @@
         child.setTag(getTagIdForProperty(property), newAnim);
     }
 
-    /** Hides children beyond the max rendering count. */
-    private void setChildrenVisibility() {
-        for (int i = 0; i < getChildCount(); i++) {
-            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);
-                }
-            }
-        }
-    }
-
     /** Return a stable ID to use as a tag key for the given property's animations. */
     private int getTagIdForProperty(DynamicAnimation.ViewProperty property) {
         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
@@ -592,7 +561,7 @@
         private View mView;
 
         /** Start velocity to use for all property animations. */
-        private float mDefaultStartVelocity = 0f;
+        private float mDefaultStartVelocity = -Float.MAX_VALUE;
 
         /** Start delay to use when start is called. */
         private long mStartDelay = 0;
@@ -625,6 +594,15 @@
          */
         private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>();
 
+        /**
+         * All of the initial property values that have been set. These values will be instantly set
+         * when {@link #start} is called, just before the animation begins.
+         */
+        private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>();
+
+        /** The animation controller that last retrieved this animator instance. */
+        private PhysicsAnimationController mAssociatedController;
+
         protected PhysicsPropertyAnimator(View view) {
             this.mView = view;
         }
@@ -644,7 +622,7 @@
 
         /** Set the view's alpha value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) {
-            mView.setAlpha(from);
+            mInitialPropertyValues.put(DynamicAnimation.ALPHA, from);
             return alpha(to, endActions);
         }
 
@@ -656,7 +634,7 @@
         /** Set the view's translationX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationX(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationX(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from);
             return translationX(to, endActions);
         }
 
@@ -668,7 +646,7 @@
         /** Set the view's translationY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator translationY(
                 float from, float to, Runnable... endActions) {
-            mView.setTranslationY(from);
+            mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from);
             return translationY(to, endActions);
         }
 
@@ -690,7 +668,7 @@
 
         /** Set the view's scaleX value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) {
-            mView.setScaleX(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from);
             return scaleX(to, endActions);
         }
 
@@ -701,7 +679,7 @@
 
         /** Set the view's scaleY value to 'from', then animate it to the given value. */
         public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) {
-            mView.setScaleY(from);
+            mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from);
             return scaleY(to, endActions);
         }
 
@@ -750,6 +728,13 @@
          * animated property on every child (including chained animations) have ended.
          */
         public void start(Runnable... after) {
+            if (!isActiveController(mAssociatedController)) {
+                Log.w(TAG, "Only the active animation controller is allowed to start animations. "
+                        + "Use PhysicsAnimationLayout#setActiveController to set the active "
+                        + "animation controller.");
+                return;
+            }
+
             final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties();
 
             // If there are end actions, set an end listener on the layout for all the properties
@@ -791,6 +776,10 @@
 
             // Actually start the animations.
             for (DynamicAnimation.ViewProperty property : properties) {
+                if (mInitialPropertyValues.containsKey(property)) {
+                    property.setValue(mView, mInitialPropertyValues.get(property));
+                }
+
                 final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
                 animateValueForChild(
                         property,
@@ -803,14 +792,7 @@
                         mEndActionsForProperty.get(property));
             }
 
-            // Clear out the animator.
-            mAnimatedProperties.clear();
-            mPositionStartVelocities.clear();
-            mDefaultStartVelocity = 0;
-            mStartDelay = 0;
-            mStiffness = -1;
-            mDampingRatio = -1;
-            mEndActionsForProperty.clear();
+            clearAnimator();
         }
 
         /** Returns the set of properties that will animate once {@link #start} is called. */
@@ -847,20 +829,50 @@
                     });
                 }
 
-                animation.getSpring().setStiffness(stiffness);
-                animation.getSpring().setDampingRatio(dampingRatio);
+                final SpringForce animationSpring = animation.getSpring();
 
-                if (startVel > 0) {
-                    animation.setStartVelocity(startVel);
+                if (animationSpring == null) {
+                    return;
                 }
 
+                final Runnable configureAndStartAnimation = () -> {
+                    animationSpring.setStiffness(stiffness);
+                    animationSpring.setDampingRatio(dampingRatio);
+
+                    if (startVel > -Float.MAX_VALUE) {
+                        animation.setStartVelocity(startVel);
+                    }
+
+                    animationSpring.setFinalPosition(value);
+                    animation.start();
+                };
+
                 if (startDelay > 0) {
-                    postDelayed(() -> animation.animateToFinalPosition(value), startDelay);
+                    postDelayed(configureAndStartAnimation, startDelay);
                 } else {
-                    animation.animateToFinalPosition(value);
+                    configureAndStartAnimation.run();
                 }
             }
         }
+
+        private void clearAnimator() {
+            mInitialPropertyValues.clear();
+            mAnimatedProperties.clear();
+            mPositionStartVelocities.clear();
+            mDefaultStartVelocity = -Float.MAX_VALUE;
+            mStartDelay = 0;
+            mStiffness = -1;
+            mDampingRatio = -1;
+            mEndActionsForProperty.clear();
+        }
+
+        /**
+         * Sets the controller that last retrieved this animator instance, so that we can prevent
+         * {@link #start} from actually starting animations if called by a non-active controller.
+         */
+        private void setAssociatedController(PhysicsAnimationController controller) {
+            mAssociatedController = controller;
+        }
     }
 
     @Override
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 7997dcb..356efc9 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -154,21 +154,6 @@
     /** Height of the status bar. */
     private float mStatusBarHeight;
 
-    @Override
-    protected void setLayout(PhysicsAnimationLayout layout) {
-        super.setLayout(layout);
-
-        Resources res = layout.getResources();
-        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
-        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
-        mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
-        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
-        mStackStartingVerticalOffset =
-                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
-        mStatusBarHeight =
-                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
-    }
-
     /**
      * Instantly move the first bubble to the given point, and animate the rest of the stack behind
      * it with the 'following' effect.
@@ -286,6 +271,8 @@
                 },
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        // If we're flinging now, there's no more touch event to catch up to.
+        mFirstBubbleSpringingToTouch = false;
         mIsMovingFromFlinging = true;
         return destinationRelativeX;
     }
@@ -656,19 +643,38 @@
 
         if (mLayout.getChildCount() > 0) {
             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
+        } else {
+            // Set the start position back to the default since we're out of bubbles. New bubbles
+            // will then animate in from the start position.
+            mStackPosition = getDefaultStartPosition();
         }
     }
 
+    @Override
+    void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+    @Override
+    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+        Resources res = layout.getResources();
+        mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+        mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+        mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
+        mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
+        mStackStartingVerticalOffset =
+                res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
+        mStatusBarHeight =
+                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
+    }
+
     /** Moves the stack, without any animation, to the starting position. */
     private void moveStackToStartPosition() {
         // Post to ensure that the layout's width and height have been calculated.
         mLayout.setVisibility(View.INVISIBLE);
         mLayout.post(() -> {
+            setStackPosition(mRestingStackPosition == null
+                    ? getDefaultStartPosition()
+                    : mRestingStackPosition);
             mStackMovedToStartPosition = true;
-            setStackPosition(
-                    mRestingStackPosition == null
-                            ? getDefaultStartPosition()
-                            : mRestingStackPosition);
             mLayout.setVisibility(View.VISIBLE);
 
             // Animate in the top bubble now that we're visible.
@@ -707,15 +713,20 @@
         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
         mStackPosition.set(pos.x, pos.y);
 
-        mLayout.cancelAllAnimations();
-        cancelStackPositionAnimations();
+        // If we're not the active controller, we don't want to physically move the bubble views.
+        if (isActiveController()) {
+            mLayout.cancelAllAnimations();
+            cancelStackPositionAnimations();
 
-        // Since we're not using the chained animations, apply the offsets manually.
-        final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
-        final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y);
-        for (int i = 0; i < mLayout.getChildCount(); i++) {
-            mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
-            mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            // Since we're not using the chained animations, apply the offsets manually.
+            final float xOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_X);
+            final float yOffset = getOffsetForChainedPropertyAnimation(
+                    DynamicAnimation.TRANSLATION_Y);
+            for (int i = 0; i < mLayout.getChildCount(); i++) {
+                mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
+                mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
+            }
         }
     }
 
@@ -732,6 +743,10 @@
 
     /** Animates in the given bubble. */
     private void animateInBubble(View child) {
+        if (!isActiveController()) {
+            return;
+        }
+
         child.setTranslationY(mStackPosition.y);
 
         float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
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 43dd4ee..10b631d 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
@@ -62,8 +62,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mExpandedController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mExpandedController);
 
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
@@ -75,14 +75,14 @@
     @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);
@@ -140,8 +140,6 @@
         assertEquals(500f, draggedBubble.getTranslationX(), 1f);
         assertEquals(500f, draggedBubble.getTranslationY(), 1f);
 
-        // Snap bubble back and make sure it returned correctly.
-        mExpandedController.prepareForDismissalWithVelocity(draggedBubble);
         mLayout.removeView(draggedBubble);
         waitForLayoutMessageQueue();
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
@@ -171,7 +169,7 @@
 
         // 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();
 
@@ -226,7 +224,7 @@
 
     /** 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);
     }
 
@@ -238,26 +236,19 @@
             assertEquals(x + i * offsetMultiplier * mStackOffset,
                     mLayout.getChildAt(i).getTranslationX(), 2f);
             assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
+            assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
 
     /** Check that children are in the correct positions for being expanded. */
     private void testBubblesInCorrectExpandedPositions() {
         // 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++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(getBubbleLeft(i),
                     mLayout.getChildAt(i).getTranslationX(),
                     2f);
             assertEquals(mExpandedController.getExpandedY(),
                     mLayout.getChildAt(i).getTranslationY(), 2f);
-
-            if (i < mMaxRenderedBubbles) {
-                assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
-            }
         }
     }
 
@@ -275,9 +266,6 @@
             return 0;
         }
         int bubbleCount = mLayout.getChildCount();
-        if (bubbleCount > mMaxRenderedBubbles) {
-            bubbleCount = mMaxRenderedBubbles;
-        }
         final float totalBubbleWidth = bubbleCount * mBubbleSize;
         final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
         final float rowWidth = totalGapWidth + totalBubbleWidth;
@@ -297,10 +285,10 @@
         final float rowMargins = (mExpandedViewPadding + launcherGridDiff) * 2;
         final float maxRowWidth = mDisplayWidth - rowMargins;
 
-        final float totalBubbleWidth = mMaxRenderedBubbles * mBubbleSize;
+        final float totalBubbleWidth = mMaxBubbles * mBubbleSize;
         final float totalGapWidth = maxRowWidth - totalBubbleWidth;
 
-        final int gapCount = mMaxRenderedBubbles - 1;
+        final int gapCount = mMaxBubbles - 1;
         final float gapWidth = totalGapWidth / gapCount;
         return gapWidth;
     }
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 eef6ddc..f8b32c2 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
@@ -23,7 +23,6 @@
 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;
 
@@ -77,21 +76,9 @@
     }
 
     @Test
-    public void testRenderVisibility() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
-
-        // The last child should be GONE, the rest VISIBLE.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
-            assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE,
-                    mLayout.getChildAt(i).getVisibility());
-        }
-    }
-
-    @Test
     public void testHierarchyChanges() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Make sure the controller was notified of all the views we added.
         for (View mView : mViews) {
@@ -115,8 +102,8 @@
 
     @Test
     public void testUpdateValueNotChained() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         // Don't chain any values.
         mTestableController.setChainedProperties(Sets.newHashSet());
@@ -146,8 +133,8 @@
 
     @Test
     public void testSetEndActions() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -189,8 +176,8 @@
 
     @Test
     public void testRemoveEndListeners() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
         mTestableController.setChainedProperties(Sets.newHashSet());
 
         final CountDownLatch xLatch = new CountDownLatch(1);
@@ -229,8 +216,8 @@
     public void testSetController() throws InterruptedException {
         // Add the bubbles, then set the controller, to make sure that a controller added to an
         // already-initialized view works correctly.
-        addOneMoreThanRenderLimitBubbles();
-        mLayout.setController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
+        mLayout.setActiveController(mTestableController);
         testChainedTranslationAnimations();
 
         TestableAnimationController secondController =
@@ -243,7 +230,7 @@
                 DynamicAnimation.SCALE_X, 10f);
         secondController.setRemoveImmediately(true);
 
-        mLayout.setController(secondController);
+        mLayout.setActiveController(secondController);
         mTestableController.animationForChildAtIndex(0)
                 .scaleX(1.5f)
                 .start();
@@ -266,7 +253,7 @@
         Mockito.verify(secondController, Mockito.atLeastOnce())
                 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
         mTestableController.animationForChildAtIndex(0)
                 .translationX(100f)
                 .start();
@@ -283,8 +270,8 @@
 
     @Test
     public void testArePropertiesAnimating() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertFalse(mLayout.arePropertiesAnimating(
                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
@@ -307,8 +294,8 @@
 
     @Test
     public void testCancelAllAnimations() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         mTestableController.animationForChildAtIndex(0)
                 .position(1000, 1000)
@@ -321,29 +308,10 @@
         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 {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
         assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
@@ -354,11 +322,7 @@
 
         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
 
-        // Since we enabled chaining, animating the first view to 100 should animate the second to
-        // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble
-        // not being visible, or animated, make sure that it has the appropriate chained
-        // translation.
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+        for (int i = 0; i < mLayout.getChildCount(); i++) {
             assertEquals(
                     100 + i * TEST_TRANSLATION_X_OFFSET,
                     mLayout.getChildAt(i).getTranslationX(), .1f);
@@ -383,8 +347,8 @@
 
     @Test
     public void testPhysicsAnimator() throws InterruptedException {
-        mLayout.setController(mTestableController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mTestableController);
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable afterAll = Mockito.mock(Runnable.class);
         Runnable after = Mockito.spy(new Runnable() {
@@ -430,9 +394,9 @@
         // Don't chain since we're going to invoke each animation independently.
         mTestableController.setChainedProperties(new HashSet<>());
 
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
-        addOneMoreThanRenderLimitBubbles();
+        addOneMoreThanBubbleLimitBubbles();
 
         Runnable allEnd = Mockito.mock(Runnable.class);
 
@@ -452,7 +416,7 @@
 
     @Test
     public void testAnimationsForChildrenFromIndex_noChildren() {
-        mLayout.setController(mTestableController);
+        mLayout.setActiveController(mTestableController);
 
         final Runnable after = Mockito.mock(Runnable.class);
         mTestableController
@@ -523,8 +487,9 @@
         }
 
         @Override
-        protected void setChildVisibility(View child, int index, int visibility) {
-            super.setChildVisibility(child, index, visibility);
-        }
+        void onChildReordered(View child, int oldIndex, int newIndex) {}
+
+        @Override
+        void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index c6acef5d..2ae759f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -56,7 +56,6 @@
 
     Handler mMainThreadHandler;
 
-    int mMaxRenderedBubbles;
     int mSystemWindowInsetSize = 50;
     int mCutoutInsetSize = 100;
 
@@ -69,6 +68,8 @@
     @Mock
     private DisplayCutout mCutout;
 
+    protected int mMaxBubbles;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -79,7 +80,7 @@
         mLayout.setTop(0);
         mLayout.setBottom(mHeight);
 
-        mMaxRenderedBubbles =
+        mMaxBubbles =
                 getContext().getResources().getInteger(R.integer.bubbles_max_rendered);
         mMainThreadHandler = new Handler(Looper.getMainLooper());
 
@@ -96,8 +97,8 @@
     }
 
     /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
-    void addOneMoreThanRenderLimitBubbles() throws InterruptedException {
-        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
+    void addOneMoreThanBubbleLimitBubbles() throws InterruptedException {
+        for (int i = 0; i < mMaxBubbles + 1; i++) {
             final View newView = new FrameLayout(mContext);
             mLayout.addView(newView, 0);
             mViews.add(0, newView);
@@ -138,6 +139,13 @@
         }
 
         @Override
+        protected boolean isActiveController(PhysicsAnimationController controller) {
+            // Return true since otherwise all test controllers will be seen as inactive since they
+            // are wrapped by MainThreadAnimationControllerWrapper.
+            return true;
+        }
+
+        @Override
         public boolean post(Runnable action) {
             return mMainThreadHandler.post(action);
         }
@@ -148,9 +156,9 @@
         }
 
         @Override
-        public void setController(PhysicsAnimationController controller) {
+        public void setActiveController(PhysicsAnimationController controller) {
             runOnMainThreadAndBlock(
-                    () -> super.setController(
+                    () -> super.setActiveController(
                             new MainThreadAnimationControllerWrapper(controller)));
         }
 
@@ -267,8 +275,15 @@
             }
 
             @Override
-            protected void setChildVisibility(View child, int index, int visibility) {
-                mWrappedController.setChildVisibility(child, index, visibility);
+            void onChildReordered(View child, int oldIndex, int newIndex) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
+            }
+
+            @Override
+            void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
+                runOnMainThreadAndBlock(
+                        () -> mWrappedController.onActiveControllerForLayout(layout));
             }
 
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 9218a8b..31a7d5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -54,8 +54,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mLayout.setController(mStackController);
-        addOneMoreThanRenderLimitBubbles();
+        mLayout.setActiveController(mStackController);
+        addOneMoreThanBubbleLimitBubbles();
         mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset);
     }