Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a08b6d30 authored by Joshua Tsuji's avatar Joshua Tsuji
Browse files

Use transient views for child removal animations.

Transient views are a non-public API but are used extensively by notification code, so it seems like a good fit here! This eliminates the need for the preceding non-removed view logic, and ensures consistent dispatch of chained animation values.

Bug: 111236845
Test: atest SystemUITests
Change-Id: I6a2988a2e31b9d709428aca707ee5d241b3a9f46
parent 580c0bf2
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -320,8 +320,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
        if (updatePosition && !mIsExpanded) {
            // If alerting it gets promoted to top of the stack.
            if (mBubbleContainer.indexOfChild(bubbleView) != 0) {
                mBubbleContainer.removeViewAndThen(bubbleView,
                        () -> mBubbleContainer.addView(bubbleView, 0));
                mBubbleContainer.moveViewTo(bubbleView, 0);
            }
            requestUpdate();
        }
+85 −68
Original line number Diff line number Diff line
@@ -27,9 +27,8 @@ import androidx.dynamicanimation.animation.SpringForce;

import com.android.systemui.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.HashSet;
import java.util.Set;

/**
@@ -122,12 +121,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
    protected final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation.OnAnimationEndListener>
            mEndListenerForProperty = new HashMap<>();

    /**
     * List of views that were passed to removeView, but are currently being animated out. These
     * views will be actually removed by the controller (via super.removeView) once they're done
     * animating out.
     */
    private final List<View> mViewsToBeActuallyRemoved = new ArrayList<>();
    /** Set of currently rendered transient views. */
    private final Set<View> mTransientViews = new HashSet<>();

    /** The currently active animation controller. */
    private PhysicsAnimationController mController;
@@ -186,23 +181,6 @@ public class PhysicsAnimationLayout extends FrameLayout {
        mEndListenerForProperty.remove(property);
    }

    /**
     * Returns the index of the view that precedes the given index, ignoring views that were passed
     * to removeView, but are currently being animated out before actually being removed.
     *
     * @return index of the preceding view, or -1 if there are none.
     */
    public int getPrecedingNonRemovedViewIndex(int index) {
        for (int i = index + 1; i < getChildCount(); i++) {
            View precedingView = getChildAt(i);
            if (!mViewsToBeActuallyRemoved.contains(precedingView)) {
                return i;
            }
        }

        return -1;
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
@@ -224,6 +202,24 @@ public class PhysicsAnimationLayout extends FrameLayout {
        removeViewAndThen(view, /* callback */ null);
    }

    @Override
    public void addTransientView(View view, int index) {
        super.addTransientView(view, index);
        mTransientViews.add(view);
    }

    @Override
    public void removeTransientView(View view) {
        super.removeTransientView(view);
        mTransientViews.remove(view);
    }

    /** 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.
@@ -231,23 +227,31 @@ public class PhysicsAnimationLayout extends FrameLayout {
    public void removeViewAndThen(View view, Runnable callback) {
        if (mController != null) {
            final int index = indexOfChild(view);
            // Remove the view only if it exists in this layout, and we're not already working on
            // animating its removal.
            if (index > -1 && !mViewsToBeActuallyRemoved.contains(view)) {
                mViewsToBeActuallyRemoved.add(view);

            // Remove the view and add it back as a transient view so we can animate it out.
            super.removeView(view);
            addTransientView(view, index);

            setChildrenVisibility();

                // Tell the controller to animate this view out, and call the callback when it wants
                // to actually remove the view.
            // Tell the controller to animate this view out, and call the callback when it's
            // finished.
            mController.onChildToBeRemoved(view, index, () -> {
                    removeViewImmediateAndThen(view, callback);
                    mViewsToBeActuallyRemoved.remove(view);
                });
                // Done animating, remove the transient view.
                removeTransientView(view);

                if (callback != null) {
                    callback.run();
                }
            });
        } else {
            // Without a controller, nobody will animate this view out, so it gets an unceremonious
            // departure.
            removeViewImmediateAndThen(view, callback);
            super.removeView(view);

            if (callback != null) {
                callback.run();
            }
        }
    }

@@ -278,17 +282,18 @@ public class PhysicsAnimationLayout extends FrameLayout {
    }

    /**
     * Animates the property of the child at the given index to the given value, then runs the
     * callback provided when the animation ends.
     * Animates the property of the given child view, then runs the callback provided when the
     * animation ends.
     */
    protected void animateValueForChildAtIndex(
    protected void animateValueForChild(
            DynamicAnimation.ViewProperty property,
            int index,
            View view,
            float value,
            float startVel,
            Runnable after) {
        if (index < getChildCount()) {
            final SpringAnimation animation = getAnimationAtIndex(property, index);
        if (view != null) {
            final SpringAnimation animation =
                    (SpringAnimation) view.getTag(getTagIdForProperty(property));
            if (after != null) {
                animation.addEndListener(new OneTimeEndListener() {
                    @Override
@@ -300,12 +305,36 @@ public class PhysicsAnimationLayout extends FrameLayout {
                });
            }

            if (startVel != Float.MAX_VALUE) {
                animation.setStartVelocity(startVel);
            animation.animateToFinalPosition(value);
        }
    }

            animation.animateToFinalPosition(value);
    protected void animateValueForChild(
            DynamicAnimation.ViewProperty property,
            View view,
            float value,
            Runnable after) {
        animateValueForChild(property, view, value, Float.MAX_VALUE, after);
    }

    protected void animateValueForChild(
            DynamicAnimation.ViewProperty property,
            View view,
            float value) {
        animateValueForChild(property, view, value, Float.MAX_VALUE, /* after */ null);
    }

    /**
     * Animates the property of the child at the given index to the given value, then runs the
     * callback provided when the animation ends.
     */
    protected void animateValueForChildAtIndex(
            DynamicAnimation.ViewProperty property,
            int index,
            float value,
            float startVel,
            Runnable after) {
        animateValueForChild(property, getChildAt(index), value, startVel, after);
    }

    /** Shortcut to animate a value with a callback, but no start velocity. */
@@ -366,18 +395,6 @@ public class PhysicsAnimationLayout extends FrameLayout {
        }
    }


    /** Immediately removes the view, without notifying the controller, then runs the callback. */
    private void removeViewImmediateAndThen(View view, Runnable callback) {
        super.removeView(view);

        if (callback != null) {
            callback.run();
        }

        setChildrenVisibility();
    }

    /**
     * Retrieves the animation of the given property from the view at the given index via the view
     * tag system.
@@ -401,7 +418,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
        newAnim.addUpdateListener((animation, value, velocity) -> {
            final int nextAnimInChain =
                    mController.getNextAnimationInChain(property, indexOfChild(child));
            if (nextAnimInChain == PhysicsAnimationController.NONE) {

            // If the controller doesn't want us to chain, or if we're a transient view in the
            // process of being removed, don't chain.
            if (nextAnimInChain == PhysicsAnimationController.NONE
                    || mTransientViews.contains(child)) {
                return;
            }

@@ -412,9 +433,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
            // 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 + mViewsToBeActuallyRemoved.size())) {
            if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) {
                getAnimationAtIndex(property, animIndex + 1)
                        .animateToFinalPosition(value + offset);
            } else if (nextAnimInChain < getChildCount()) {
@@ -442,9 +461,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
                    // 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 + mViewsToBeActuallyRemoved.size())
                        ? View.VISIBLE
                        : View.GONE);
                    i < mMaxRenderedChildren ? View.VISIBLE : View.GONE);
        }
    }

+16 −24
Original line number Diff line number Diff line
@@ -53,8 +53,8 @@ public class StackAnimationController extends
    /** Scale factor to use initially for new bubbles being animated in. */
    private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;

    /** Translation factor (multiplied by stack offset) to use for new bubbles being animated in. */
    private static final int ANIMATE_IN_TRANSLATION_FACTOR = 4;
    /** 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 the default {@link SpringForce} provided to the physics animation layout.
@@ -309,7 +309,7 @@ public class StackAnimationController extends
            // animate in from this position. Since the animations are chained, when the new bubble
            // flies in from the side, it will push the other ones out of the way.
            float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
            child.setTranslationX(mStackPosition.x - (ANIMATE_IN_TRANSLATION_FACTOR * xOffset));
            child.setTranslationX(mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset);
            mLayout.animateValueForChildAtIndex(
                    DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
        }
@@ -318,27 +318,19 @@ public class StackAnimationController extends
    @Override
    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
        // Animate the child out, actually removing it once its alpha is zero.
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.ALPHA, index, 0f, () -> {
                    actuallyRemove.run();
                });
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.SCALE_X, index, ANIMATE_IN_STARTING_SCALE);
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.SCALE_Y, index, ANIMATE_IN_STARTING_SCALE);
        mLayout.animateValueForChild(
                DynamicAnimation.ALPHA, child, 0f, actuallyRemove);
        mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_IN_STARTING_SCALE);
        mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_IN_STARTING_SCALE);

        final boolean hasPrecedingChild = index + 1 < mLayout.getChildCount();
        if (hasPrecedingChild) {
            final int precedingViewIndex = mLayout.getPrecedingNonRemovedViewIndex(index);
            if (precedingViewIndex >= 0) {
                final float offsetX =
                        getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
                mLayout.animatePositionForChildAtIndex(
                        precedingViewIndex,
                        mStackPosition.x + (index * offsetX),
                        mStackPosition.y);
            }
        }
        // Animate the removing view in the opposite direction of the stack.
        final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
        mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_X, child,
                mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR));

        // Pull the top of the stack to the correct position, the chained animations will instruct
        // any children that are out of place to animate to the correct position.
        mLayout.animateValueForChildAtIndex(DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
    }

    /** Moves the stack, without any animation, to the starting position. */
+2 −24
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
    }

    @Test
    public void testRenderVisibility() {
    public void testRenderVisibility() throws InterruptedException {
        mLayout.setController(mTestableController);
        addOneMoreThanRenderLimitBubbles();

@@ -87,7 +87,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
    }

    @Test
    public void testHierarchyChanges() {
    public void testHierarchyChanges() throws InterruptedException {
        mLayout.setController(mTestableController);
        addOneMoreThanRenderLimitBubbles();

@@ -241,26 +241,6 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
        endListenerCalls.verifyNoMoreInteractions();
    }

    @Test
    public void testPrecedingNonRemovedIndex() {
        mLayout.setController(mTestableController);
        addOneMoreThanRenderLimitBubbles();

        // Call removeView at index 4, but don't actually remove it yet (as if we're animating it
        // out). The preceding, non-removed view index to 3 should initially be 4, but then 5 since
        // 4 is on its way out.
        assertEquals(4, mLayout.getPrecedingNonRemovedViewIndex(3));
        mLayout.removeView(mViews.get(4));
        assertEquals(5, mLayout.getPrecedingNonRemovedViewIndex(3));

        // Call removeView at index 1, and actually remove it immediately. With the old view at 1
        // instantly gone, the preceding view to 0 should be 1 in both cases.
        assertEquals(1, mLayout.getPrecedingNonRemovedViewIndex(0));
        mTestableController.setRemoveImmediately(true);
        mLayout.removeView(mViews.get(1));
        assertEquals(1, mLayout.getPrecedingNonRemovedViewIndex(0));
    }

    @Test
    public void testSetController() throws InterruptedException {
        // Add the bubbles, then set the controller, to make sure that a controller added to an
@@ -360,8 +340,6 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {

        mLayout.cancelAllAnimations();

        waitForLayoutMessageQueue();

        // Animations should be somewhere before their end point.
        assertTrue(mViews.get(0).getTranslationX() < 1000);
        assertTrue(mViews.get(0).getTranslationY() < 1000);
+30 −7
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.os.Handler;
import android.os.Looper;
import android.view.DisplayCutout;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;

@@ -93,7 +94,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
    }

    /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
    void addOneMoreThanRenderLimitBubbles() {
    void addOneMoreThanRenderLimitBubbles() throws InterruptedException {
        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
            final View newView = new FrameLayout(mContext);
            mLayout.addView(newView, 0);
@@ -129,7 +130,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
    void waitForLayoutMessageQueue() throws InterruptedException {
        // Wait for layout, then the view should be actually removed.
        CountDownLatch layoutLatch = new CountDownLatch(1);
        mLayout.post(layoutLatch::countDown);
        mMainThreadHandler.post(layoutLatch::countDown);
        layoutLatch.await(1, TimeUnit.SECONDS);
    }

@@ -145,11 +146,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
        @Override
        public void setController(PhysicsAnimationController controller) {
            mMainThreadHandler.post(() -> super.setController(controller));
            try {
                waitForLayoutMessageQueue();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            waitForMessageQueueAndIgnoreIfInterrupted();
        }

        @Override
@@ -169,6 +166,32 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
            return mWindowInsets;
        }

        @Override
        public void removeView(View view) {
            mMainThreadHandler.post(() ->
                    super.removeView(view));
            waitForMessageQueueAndIgnoreIfInterrupted();
        }

        @Override
        public void addView(View child, int index, ViewGroup.LayoutParams params) {
            mMainThreadHandler.post(() ->
                    super.addView(child, index, params));
            waitForMessageQueueAndIgnoreIfInterrupted();
        }

        /**
         * Wait for the queue but just catch and print the exception if interrupted, since we can't
         * just add the exception to the overridden methods' signatures.
         */
        private void waitForMessageQueueAndIgnoreIfInterrupted() {
            try {
                waitForLayoutMessageQueue();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        /**
         * Sets an end listener that will be called after the 'real' end listener that was already
         * set.
Loading