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

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

Fix issues with animations when the stack is expanded.

- Don't show the flyout if the stack has become expanded.
- Add onChildReordered so that the controllers don't receive onChildAdded calls when views are reordered. This was causing animation issues with expanded bubbles.
- Clear the dragging out bubble onGestureFinished, so that it's not ignored by animations.
- Update the expand/collapse animations when a new child is added during animation, so that it'll also animate to the proper spot.

Fixes: 129370170
Bug: 123542488
Test: atest SystemUITests
Change-Id: I1360deb09db82bd3ba72cb91fc9abe05b6dc1a9c
parent 4bb3e7e7
Loading
Loading
Loading
Loading
+24 −13
Original line number Diff line number Diff line
@@ -726,7 +726,7 @@ public class BubbleStackView extends FrameLayout {
    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);
        }
    }

@@ -908,16 +908,14 @@ public class BubbleStackView extends FrameLayout {

            if (shouldExpand) {
                mBubbleContainer.setActiveController(mExpandedAnimationController);
                mExpandedAnimationController.expandFromStack(
                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
                        /* collapseTo */,
                        () -> {
                mExpandedAnimationController.expandFromStack(() -> {
                    updatePointerPosition();
                    updateAfter.run();
                } /* after */);
            } else {
                mBubbleContainer.cancelAllAnimations();
                mExpandedAnimationController.collapseBackToStack(
                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
                        () -> {
                            mBubbleContainer.setActiveController(mStackAnimationController);
                            updateAfter.run();
@@ -1110,6 +1108,10 @@ public class BubbleStackView extends FrameLayout {
    /** 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. */
@@ -1200,6 +1202,7 @@ public class BubbleStackView extends FrameLayout {
     */
    void magnetToStackIfNeededThenAnimateDismissal(
            View touchedView, float velX, float velY, Runnable after) {
        final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
        final Runnable animateDismissal = () -> {
            mAfterMagnet = null;

@@ -1217,7 +1220,7 @@ public class BubbleStackView extends FrameLayout {
                            resetDesaturationAndDarken();
                        });
            } else {
                mExpandedAnimationController.dismissDraggedOutBubble(() -> {
                mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
                    mAnimatingMagnet = false;
                    mShowingDismiss = false;
                    mDraggingInDismissTarget = false;
@@ -1384,10 +1387,18 @@ public class BubbleStackView extends FrameLayout {
                };

                // Post in case layout isn't complete and getWidth returns 0.
                post(() -> mFlyout.showFlyout(
                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));
                            bubble.iconView.getBadgeColor(), mAfterFlyoutHides);
                });
            }

            mFlyout.removeCallbacks(mHideFlyout);
+86 −47
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.graphics.PointF;
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 @@ public class ExpandedAnimationController
    /** 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,43 +101,61 @@ public class ExpandedAnimationController
    /** The bubble currently being dragged out of the row (to potentially be dismissed). */
    private View mBubbleDraggingOut;

    /**
     * Drag velocities for the dragging-out bubble when the drag finished. These are used by
     * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
     */
    private float mBubbleDraggingOutVelX;
    private float mBubbleDraggingOutVelY;

    /**
     * Animates expanding the bubbles into a row along the top of the screen.
     */
    public void expandFromStack(PointF collapseTo, Runnable after) {
    public void expandFromStack(Runnable after) {
        mAnimatingCollapse = false;
        mAnimatingExpand = true;
        mAfterExpand = after;

        startOrUpdateExpandAnimation();
    }

    /** Animate collapsing the bubbles back to their stacked position. */
    public void collapseBackToStack(PointF collapsePoint, Runnable after) {
        mAnimatingExpand = false;
        mAnimatingCollapse = true;
        mAfterCollapse = after;
        mCollapsePoint = collapsePoint;

        startOrUpdateCollapseAnimation();
    }

    private void startOrUpdateExpandAnimation() {
        animationsForChildrenFromIndex(
                0, /* startIndex */
                new ChildAnimationConfigurator() {
                    @Override
                    public void configureAnimationForChildAtIndex(
                            int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) {
                        animation.position(getBubbleLeft(index), getExpandedY());
                (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
                .startAll(() -> {
                    mAnimatingExpand = false;

                    if (mAfterExpand != null) {
                        mAfterExpand.run();
                    }
            })
            .startAll(after);

        mCollapseToPoint = collapseTo;
                    mAfterExpand = null;
                });
    }

    /** Animate collapsing the bubbles back to their stacked position. */
    public void collapseBackToStack(Runnable after) {
    private void startOrUpdateCollapseAnimation() {
        // Stack to the left if we're going to the left, or right if not.
        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;

        final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
        animationsForChildrenFromIndex(
                0, /* startIndex */
                (index, animation) ->
                (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. */
@@ -174,10 +199,10 @@ public class ExpandedAnimationController
    }

    /** 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)
@@ -187,6 +212,10 @@ public class ExpandedAnimationController
        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) {
@@ -229,20 +258,13 @@ public class ExpandedAnimationController
                .withPositionStartVelocities(velX, velY)
                .start(() -> bubbleView.setTranslationZ(0f) /* after */);

        mBubbleDraggingOut = null;
        mBubbleDraggedOutEnough = false;
        updateBubblePositions();
    }

    /**
     * Sets configuration variables so that when the given bubble is removed, the animations are
     * started with the given velocities.
     */
    public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
        mBubbleDraggingOut = bubbleView;
        mBubbleDraggingOutVelX = velX;
        mBubbleDraggingOutVelY = velY;
    /** Resets bubble drag out gesture flags. */
    public void onGestureFinished() {
        mBubbleDraggedOutEnough = false;
        mBubbleDraggingOut = null;
    }

    /**
@@ -326,8 +348,14 @@ public class ExpandedAnimationController

    @Override
    void onChildAdded(View child, int index) {
        // 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(getXForChildAtIndex(index));

            animationForChild(child)
                    .translationY(
                            getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
@@ -335,6 +363,7 @@ public class ExpandedAnimationController
                    .start();
            updateBubblePositions();
        }
    }

    @Override
    void onChildRemoved(View child, int index, Runnable finishRemoval) {
@@ -357,7 +386,16 @@ public class ExpandedAnimationController
        updateBubblePositions();
    }

    @Override
    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);

@@ -366,6 +404,7 @@ public class ExpandedAnimationController
            if (bubble.equals(mBubbleDraggingOut)) {
                return;
            }

            animationForChild(bubble)
                    .translationX(getBubbleLeft(i))
                    .start();
+40 −37
Original line number Diff line number Diff line
@@ -139,6 +139,9 @@ public class PhysicsAnimationLayout extends FrameLayout {
         */
        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
@@ -311,40 +314,11 @@ public class PhysicsAnimationLayout extends FrameLayout {

    @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 setActiveController is called.
        if (mController != null) {
            for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
                setUpAnimationForChild(property, child, index);
            }

            mController.onChildAdded(child, index);
        }
        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);

@@ -359,19 +333,28 @@ public class PhysicsAnimationLayout extends FrameLayout {
                // 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);
        }
    }

@@ -452,6 +435,26 @@ public class PhysicsAnimationLayout extends FrameLayout {
        }
    }

    /**
     * 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.
+3 −0
Original line number Diff line number Diff line
@@ -650,6 +650,9 @@ public class StackAnimationController extends
        }
    }

    @Override
    void onChildReordered(View child, int oldIndex, int newIndex) {}

    @Override
    void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
        Resources res = layout.getResources();
+4 −5
Original line number Diff line number Diff line
@@ -73,14 +73,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
    @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);
@@ -139,7 +139,6 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
        assertEquals(500f, draggedBubble.getTranslationY(), 1f);

        // Snap it back and make sure it made it back correctly.
        mExpandedController.prepareForDismissalWithVelocity(draggedBubble, 0f, 0f);
        mLayout.removeView(draggedBubble);
        waitForLayoutMessageQueue();
        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
@@ -169,7 +168,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC

        // 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();

@@ -224,7 +223,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC

    /** 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);
    }

Loading