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

Commit 7b622a6a authored by Josh Tsuji's avatar Josh Tsuji Committed by Android (Google) Code Review
Browse files

Merge "Use transient views for child removal animations."

parents 62c8b56d a08b6d30
Loading
Loading
Loading
Loading
+1 −2
Original line number Original line Diff line number Diff line
@@ -324,8 +324,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
        if (updatePosition && !mIsExpanded) {
        if (updatePosition && !mIsExpanded) {
            // If alerting it gets promoted to top of the stack.
            // If alerting it gets promoted to top of the stack.
            if (mBubbleContainer.indexOfChild(bubbleView) != 0) {
            if (mBubbleContainer.indexOfChild(bubbleView) != 0) {
                mBubbleContainer.removeViewAndThen(bubbleView,
                mBubbleContainer.moveViewTo(bubbleView, 0);
                        () -> mBubbleContainer.addView(bubbleView, 0));
            }
            }
            requestUpdate();
            requestUpdate();
        }
        }
+85 −68
Original line number Original line Diff line number Diff line
@@ -27,9 +27,8 @@ import androidx.dynamicanimation.animation.SpringForce;


import com.android.systemui.R;
import com.android.systemui.R;


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


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


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


    /** The currently active animation controller. */
    /** The currently active animation controller. */
    private PhysicsAnimationController mController;
    private PhysicsAnimationController mController;
@@ -186,23 +181,6 @@ public class PhysicsAnimationLayout extends FrameLayout {
        mEndListenerForProperty.remove(property);
        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
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        super.addView(child, index, params);
@@ -224,6 +202,24 @@ public class PhysicsAnimationLayout extends FrameLayout {
        removeViewAndThen(view, /* callback */ null);
        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
     * 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.
     * 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) {
    public void removeViewAndThen(View view, Runnable callback) {
        if (mController != null) {
        if (mController != null) {
            final int index = indexOfChild(view);
            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.
            // Remove the view and add it back as a transient view so we can animate it out.
            if (index > -1 && !mViewsToBeActuallyRemoved.contains(view)) {
            super.removeView(view);
                mViewsToBeActuallyRemoved.add(view);
            addTransientView(view, index);

            setChildrenVisibility();
            setChildrenVisibility();


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

                if (callback != null) {
                    callback.run();
                }
                }
            });
        } else {
        } else {
            // Without a controller, nobody will animate this view out, so it gets an unceremonious
            // Without a controller, nobody will animate this view out, so it gets an unceremonious
            // departure.
            // 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
     * Animates the property of the given child view, then runs the callback provided when the
     * callback provided when the animation ends.
     * animation ends.
     */
     */
    protected void animateValueForChildAtIndex(
    protected void animateValueForChild(
            DynamicAnimation.ViewProperty property,
            DynamicAnimation.ViewProperty property,
            int index,
            View view,
            float value,
            float value,
            float startVel,
            float startVel,
            Runnable after) {
            Runnable after) {
        if (index < getChildCount()) {
        if (view != null) {
            final SpringAnimation animation = getAnimationAtIndex(property, index);
            final SpringAnimation animation =
                    (SpringAnimation) view.getTag(getTagIdForProperty(property));
            if (after != null) {
            if (after != null) {
                animation.addEndListener(new OneTimeEndListener() {
                animation.addEndListener(new OneTimeEndListener() {
                    @Override
                    @Override
@@ -300,12 +305,36 @@ public class PhysicsAnimationLayout extends FrameLayout {
                });
                });
            }
            }


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


            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. */
    /** 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
     * Retrieves the animation of the given property from the view at the given index via the view
     * tag system.
     * tag system.
@@ -401,7 +418,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
        newAnim.addUpdateListener((animation, value, velocity) -> {
        newAnim.addUpdateListener((animation, value, velocity) -> {
            final int nextAnimInChain =
            final int nextAnimInChain =
                    mController.getNextAnimationInChain(property, indexOfChild(child));
                    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;
                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
            // 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
            // subsequent animation within the rendering limit, and if so, tell it to animate to
            // this animation's new value (plus the offset).
            // this animation's new value (plus the offset).
            if (nextAnimInChain < Math.min(
            if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) {
                    getChildCount(),
                    mMaxRenderedChildren + mViewsToBeActuallyRemoved.size())) {
                getAnimationAtIndex(property, animIndex + 1)
                getAnimationAtIndex(property, animIndex + 1)
                        .animateToFinalPosition(value + offset);
                        .animateToFinalPosition(value + offset);
            } else if (nextAnimInChain < getChildCount()) {
            } 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
                    // 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
                    // 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.
                    // and will soon be removed, render up to 9 views temporarily.
                    i < (mMaxRenderedChildren + mViewsToBeActuallyRemoved.size())
                    i < mMaxRenderedChildren ? View.VISIBLE : View.GONE);
                        ? View.VISIBLE
                        : View.GONE);
        }
        }
    }
    }


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


    /** Translation factor (multiplied by stack offset) to use for new bubbles being animated in. */
    /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
    private static final int ANIMATE_IN_TRANSLATION_FACTOR = 4;
    private static final int ANIMATE_TRANSLATION_FACTOR = 4;


    /**
    /**
     * Values to use for the default {@link SpringForce} provided to the physics animation layout.
     * 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
            // 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.
            // flies in from the side, it will push the other ones out of the way.
            float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
            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(
            mLayout.animateValueForChildAtIndex(
                    DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
                    DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
        }
        }
@@ -318,27 +318,19 @@ public class StackAnimationController extends
    @Override
    @Override
    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
        // Animate the child out, actually removing it once its alpha is zero.
        // Animate the child out, actually removing it once its alpha is zero.
        mLayout.animateValueForChildAtIndex(
        mLayout.animateValueForChild(
                DynamicAnimation.ALPHA, index, 0f, () -> {
                DynamicAnimation.ALPHA, child, 0f, actuallyRemove);
                    actuallyRemove.run();
        mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_IN_STARTING_SCALE);
                });
        mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_IN_STARTING_SCALE);
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.SCALE_X, index, ANIMATE_IN_STARTING_SCALE);
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.SCALE_Y, index, ANIMATE_IN_STARTING_SCALE);


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

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


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


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


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


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


@@ -241,26 +241,6 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
        endListenerCalls.verifyNoMoreInteractions();
        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
    @Test
    public void testSetController() throws InterruptedException {
    public void testSetController() throws InterruptedException {
        // Add the bubbles, then set the controller, to make sure that a controller added to an
        // 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();
        mLayout.cancelAllAnimations();


        waitForLayoutMessageQueue();

        // Animations should be somewhere before their end point.
        // Animations should be somewhere before their end point.
        assertTrue(mViews.get(0).getTranslationX() < 1000);
        assertTrue(mViews.get(0).getTranslationX() < 1000);
        assertTrue(mViews.get(0).getTranslationY() < 1000);
        assertTrue(mViews.get(0).getTranslationY() < 1000);
+30 −7
Original line number Original line Diff line number Diff line
@@ -23,6 +23,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Looper;
import android.view.DisplayCutout;
import android.view.DisplayCutout;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowInsets;
import android.widget.FrameLayout;
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. */
    /** 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++) {
        for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
            final View newView = new FrameLayout(mContext);
            final View newView = new FrameLayout(mContext);
            mLayout.addView(newView, 0);
            mLayout.addView(newView, 0);
@@ -129,7 +130,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
    void waitForLayoutMessageQueue() throws InterruptedException {
    void waitForLayoutMessageQueue() throws InterruptedException {
        // Wait for layout, then the view should be actually removed.
        // Wait for layout, then the view should be actually removed.
        CountDownLatch layoutLatch = new CountDownLatch(1);
        CountDownLatch layoutLatch = new CountDownLatch(1);
        mLayout.post(layoutLatch::countDown);
        mMainThreadHandler.post(layoutLatch::countDown);
        layoutLatch.await(1, TimeUnit.SECONDS);
        layoutLatch.await(1, TimeUnit.SECONDS);
    }
    }


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


        @Override
        @Override
@@ -169,6 +166,32 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
            return mWindowInsets;
            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
         * Sets an end listener that will be called after the 'real' end listener that was already
         * set.
         * set.
Loading