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

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

Merge "Update bubble + flyout animations." into qt-r1-bubbles-dev

parents 19d14536 14e68558
Loading
Loading
Loading
Loading
+14 −22
Original line number Diff line number Diff line
@@ -39,8 +39,7 @@ import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
import android.widget.TextView;

import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.annotation.Nullable;

import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape;
@@ -67,9 +66,6 @@ public class BubbleFlyoutView extends FrameLayout {

    private final ViewGroup mFlyoutTextContainer;
    private final TextView mFlyoutText;
    /** Spring animation for the flyout. */
    private final SpringAnimation mFlyoutSpring =
            new SpringAnimation(this, DynamicAnimation.TRANSLATION_X);

    /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */
    private final float mNewDotRadius;
@@ -141,7 +137,7 @@ public class BubbleFlyoutView extends FrameLayout {
    private static final float DOT_SCALE = 0.8f;

    /** Callback to run when the flyout is hidden. */
    private Runnable mOnHide;
    @Nullable private Runnable mOnHide;

    public BubbleFlyoutView(Context context) {
        super(context);
@@ -209,17 +205,16 @@ public class BubbleFlyoutView extends FrameLayout {
        super.onDraw(canvas);
    }

    /** Configures the flyout and animates it in. */
    void showFlyout(
    /** Configures the flyout, collapsed into to dot form. */
    void setupFlyoutStartingAsDot(
            CharSequence updateMessage, PointF stackPos, float parentWidth,
            boolean arrowPointingLeft, int dotColor, Runnable onHide) {
            boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete,
            @Nullable Runnable onHide) {
        mArrowPointingLeft = arrowPointingLeft;
        mDotColor = dotColor;
        mOnHide = onHide;

        setCollapsePercent(0f);
        setAlpha(0f);
        setVisibility(VISIBLE);
        setCollapsePercent(1f);

        // Set the flyout TextView's max width in terms of percent, and then subtract out the
        // padding so that the entire flyout view will be the desired width (rather than the
@@ -245,14 +240,6 @@ public class BubbleFlyoutView extends FrameLayout {
                    ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble
                    : stackPos.x - getWidth() - mFlyoutSpaceFromBubble;

            // Translate towards the stack slightly.
            setTranslationX(
                    mRestingTranslationX + (arrowPointingLeft ? -mBubbleSize : mBubbleSize));

            // Fade in the entire flyout and spring it to its normal position.
            animate().alpha(1f);
            mFlyoutSpring.animateToFinalPosition(mRestingTranslationX);

            // Calculate the difference in size between the flyout and the 'dot' so that we can
            // transform into the dot later.
            mFlyoutToDotWidthDelta = getWidth() - mNewDotSize;
@@ -271,12 +258,17 @@ public class BubbleFlyoutView extends FrameLayout {
                    getHeight() / 2f
                            - mBubbleSize / 2f
                            + mOriginalDotSize / 2;

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

    /**
     * Hides the flyout and runs the optional callback passed into showFlyout. The flyout has been
     * animated into the 'new' dot by the time we call this, so no animations are needed.
     * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot.
     * The flyout has been animated into the 'new' dot by the time we call this, so no animations
     * are needed.
     */
    void hideFlyout() {
        if (mOnHide != null) {
+60 −10
Original line number Diff line number Diff line
@@ -287,6 +287,11 @@ public class BubbleStackView extends FrameLayout {
    /** Distance the flyout has been dragged in the X axis. */
    private float mFlyoutDragDeltaX = 0f;

    /**
     * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
     */
    private Runnable mAnimateInFlyout;

    /**
     * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
     * it immediately.
@@ -370,7 +375,7 @@ public class BubbleStackView extends FrameLayout {
        addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));

        mFlyoutTransitionSpring.setSpring(new SpringForce()
                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
                .setStiffness(SpringForce.STIFFNESS_LOW)
                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
        mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);

@@ -1091,6 +1096,19 @@ public class BubbleStackView extends FrameLayout {
        mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
    }

    /**
     * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
     * once it collapses.
     */
    @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null;

    void onFlyoutTapped() {
        mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();

        mFlyout.removeCallbacks(mHideFlyout);
        mHideFlyout.run();
    }

    /**
     * Called when the flyout drag has finished, and returns true if the gesture successfully
     * dismissed the flyout.
@@ -1288,6 +1306,12 @@ public class BubbleStackView extends FrameLayout {
    /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
    private void animateFlyoutCollapsed(boolean collapsed, float velX) {
        final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
        // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
        // faster.
        mFlyoutTransitionSpring.getSpring().setStiffness(
                (mBubbleToExpandAfterFlyoutCollapse != null)
                        ? SpringForce.STIFFNESS_MEDIUM
                        : SpringForce.STIFFNESS_LOW);
        mFlyoutTransitionSpring
                .setStartValue(mFlyoutDragDeltaX)
                .setStartVelocity(velX)
@@ -1329,8 +1353,10 @@ public class BubbleStackView extends FrameLayout {
        if (updateMessage == null
                || isExpanded()
                || mIsExpansionAnimating
                || mIsGestureInProgress) {
            // Skip the message if none exists, we're expanded or animating expansion.
                || mIsGestureInProgress
                || mBubbleToExpandAfterFlyoutCollapse != null) {
            // Skip the message if none exists, we're expanded or animating expansion, or we're
            // about to expand a bubble from the previous tapped flyout.
            return;
        }

@@ -1339,18 +1365,14 @@ public class BubbleStackView extends FrameLayout {
            bubble.getIconView().setSuppressDot(
                    true /* suppressDot */, false /* animate */);

            mFlyout.removeCallbacks(mAnimateInFlyout);
            mFlyoutDragDeltaX = 0f;
            mFlyout.setAlpha(0f);

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

            mAfterFlyoutHides = () -> {
                if (bubble.getIconView() == null) {
                    return;
                }

                final boolean suppressDot = !bubble.showBubbleDot();
                // If we're going to suppress the dot, make it visible first so it'll
                // visibly animate away.
@@ -1365,8 +1387,16 @@ public class BubbleStackView extends FrameLayout {
                bubble.getIconView().setSuppressDot(
                        suppressDot /* suppressDot */,
                        suppressDot /* animate */);

                if (mBubbleToExpandAfterFlyoutCollapse != null) {
                    mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
                    mBubbleData.setExpanded(true);
                    mBubbleToExpandAfterFlyoutCollapse = null;
                }
            };

            mFlyout.setVisibility(INVISIBLE);

            // Post in case layout isn't complete and getWidth returns 0.
            post(() -> {
                // An auto-expanding bubble could have been posted during the time it takes to
@@ -1375,10 +1405,29 @@ public class BubbleStackView extends FrameLayout {
                    return;
                }

                mFlyout.showFlyout(
                final Runnable afterShow = () -> {
                    mAnimateInFlyout = () -> {
                        mFlyout.setVisibility(VISIBLE);
                        bubble.getIconView().setSuppressDot(
                                true /* suppressDot */, false /* animate */);
                        mFlyoutDragDeltaX =
                                mStackAnimationController.isStackOnLeftSide()
                                        ? -mFlyout.getWidth()
                                        : mFlyout.getWidth();
                        animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
                        mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
                    };

                    mFlyout.postDelayed(mAnimateInFlyout, 200);
                };

                mFlyout.setupFlyoutStartingAsDot(
                        updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
                        mStackAnimationController.isStackOnLeftSide(),
                        bubble.getIconView().getBadgeColor(), mAfterFlyoutHides);
                        bubble.getIconView().getBadgeColor(),
                        afterShow,
                        mAfterFlyoutHides);
                mFlyout.bringToFront();
            });
        }

@@ -1393,6 +1442,7 @@ public class BubbleStackView extends FrameLayout {
            mAfterFlyoutHides.run();
        }

        mFlyout.removeCallbacks(mAnimateInFlyout);
        mFlyout.removeCallbacks(mHideFlyout);
        mFlyout.hideFlyout();
    }
+1 −2
Original line number Diff line number Diff line
@@ -192,9 +192,8 @@ class BubbleTouchHandler implements View.OnTouchListener {
                                }
                            });
                } else if (isFlyout) {
                    // TODO(b/129768381): Expand if tapped, dismiss if swiped away.
                    if (!mBubbleData.isExpanded() && !mMovedEnough) {
                        mBubbleData.setExpanded(true);
                        mStack.onFlyoutTapped();
                    }
                } else if (mMovedEnough) {
                    if (isStack) {
+27 −10
Original line number Diff line number Diff line
@@ -56,6 +56,10 @@ public class StackAnimationController extends
    /** 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 animating bubbles in. */
    private static final float ANIMATE_IN_STIFFNESS = 1000f;
    private static final int ANIMATE_IN_START_DELAY = 25;

    /**
     * Values to use for the default {@link SpringForce} provided to the physics animation layout.
     */
@@ -643,7 +647,7 @@ public class StackAnimationController extends
        } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
            // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
            // to the back of the stack, it'll be largely invisible so don't bother animating it in.
            animateInBubble(child);
            animateInBubble(child, index);
        }
    }

@@ -697,7 +701,7 @@ public class StackAnimationController extends

            // Animate in the top bubble now that we're visible.
            if (mLayout.getChildCount() > 0) {
                animateInBubble(mLayout.getChildAt(0));
                animateInBubble(mLayout.getChildAt(0), 0 /* index */);
            }
        });
    }
@@ -760,21 +764,34 @@ public class StackAnimationController extends
    }

    /** Animates in the given bubble. */
    private void animateInBubble(View child) {
    private void animateInBubble(View child, int index) {
        if (!isActiveController()) {
            return;
        }

        final float xOffset =
                getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);

        // Position the new bubble in the correct position, scaled down completely.
        child.setTranslationX(mStackPosition.x + xOffset * index);
        child.setTranslationY(mStackPosition.y);
        child.setScaleX(0f);
        child.setScaleY(0f);

        // Push the subsequent views out of the way, if there are subsequent views.
        if (index + 1 < mLayout.getChildCount()) {
            animationForChildAtIndex(index + 1)
                    .translationX(mStackPosition.x + xOffset * (index + 1))
                    .withStiffness(SpringForce.STIFFNESS_LOW)
                    .start();
        }

        float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
        // Scale in the new bubble, slightly delayed.
        animationForChild(child)
                .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
                .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
                .alpha(0f /* from */, 1f /* to */)
                .translationX(
                        mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */,
                        mStackPosition.x /* to */)
                .scaleX(1f)
                .scaleY(1f)
                .withStiffness(ANIMATE_IN_STIFFNESS)
                .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0)
                .start();
    }

+9 −10
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.systemui.bubbles;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Mockito.verify;

@@ -57,16 +56,19 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {

    @Test
    public void testShowFlyout_isVisible() {
        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null);
        mFlyout.setupFlyoutStartingAsDot(
                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null);
        mFlyout.setVisibility(View.VISIBLE);

        assertEquals("Hello", mFlyoutText.getText());
        assertEquals(View.VISIBLE, mFlyout.getVisibility());
        assertEquals(1f, mFlyoutText.getAlpha(), .01f);
    }

    @Test
    public void testFlyoutHide_runsCallback() {
        Runnable after = Mockito.mock(Runnable.class);
        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, after);
        mFlyout.setupFlyoutStartingAsDot(
                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after);
        mFlyout.hideFlyout();

        verify(after).run();
@@ -74,19 +76,16 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {

    @Test
    public void testSetCollapsePercent() {
        mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null);

        float initialTranslationZ = mFlyout.getTranslationZ();
        mFlyout.setupFlyoutStartingAsDot(
                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null);
        mFlyout.setVisibility(View.VISIBLE);

        mFlyout.setCollapsePercent(1f);
        assertEquals(0f, mFlyoutText.getAlpha(), 0.01f);
        assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse.
        assertTrue(mFlyout.getTranslationZ() < initialTranslationZ); // Should be descending.

        mFlyout.setCollapsePercent(0f);
        assertEquals(1f, mFlyoutText.getAlpha(), 0.01f);
        assertEquals(0f, mFlyoutText.getTranslationX());
        assertEquals(initialTranslationZ, mFlyout.getTranslationZ());

    }
}