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

Commit 048094f6 authored by Lyn Han's avatar Lyn Han
Browse files

Consecutive flyout animation

- fade out old message
- fade in new message
- vertical-center flyout w.r.t bubble

Bug: 170267642

Test: send single/group message
  => dot to flyout (and reverse) animation ok

Test: send consecutive messages from same/diff bubble
  => fade animation ok

Test: send multi-line message, then single line message
  => flyout updates vertical-center w.r.t bubble

Test: drag flyout to dot, tap flyout, repeat tests on right side
  => no regressions

Change-Id: I3e87c0ffebd27e1974b12ef4ad69bd1f627122ab
parent a3e52bf4
Loading
Loading
Loading
Loading
+63 −29
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.bubbles;

import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static com.android.systemui.Interpolators.ALPHA_IN;
import static com.android.systemui.Interpolators.ALPHA_OUT;

import android.animation.ArgbEvaluator;
import android.content.Context;
@@ -56,6 +58,11 @@ public class BubbleFlyoutView extends FrameLayout {
    /** Max width of the flyout, in terms of percent of the screen width. */
    private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;

    /** Translation Y of fade animation. */
    private static final float FLYOUT_FADE_Y = 40f;

    private static final long FLYOUT_FADE_DURATION = 200L;

    private final int mFlyoutPadding;
    private final int mFlyoutSpaceFromBubble;
    private final int mPointerSize;
@@ -104,6 +111,9 @@ public class BubbleFlyoutView extends FrameLayout {
    /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */
    private final RectF mBgRect = new RectF();

    /** The y position of the flyout, relative to the top of the screen. */
    private float mFlyoutY = 0f;

    /**
     * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse
     * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code
@@ -221,18 +231,33 @@ public class BubbleFlyoutView extends FrameLayout {
        mSenderText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize);
    }

    /** Configures the flyout, collapsed into to dot form. */
    void setupFlyoutStartingAsDot(
            Bubble.FlyoutMessage flyoutMessage,
            PointF stackPos,
            float parentWidth,
            boolean arrowPointingLeft,
            int dotColor,
            @Nullable Runnable onLayoutComplete,
            @Nullable Runnable onHide,
            float[] dotCenter,
            boolean hideDot) {
    /*
     * Fade animation for consecutive flyouts.
     */
    void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, float stackY) {
        fade(false /* in */);
        updateFlyoutMessage(flyoutMessage, parentWidth);
        // Wait for TextViews to layout with updated height.
        post(() -> {
            mFlyoutY = stackY + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f;
            fade(true /* in */);
        });
    }

    private void fade(boolean in) {
        setAlpha(in ? 0f : 1f);
        setTranslationY(in ? mFlyoutY : mFlyoutY + FLYOUT_FADE_Y);
        animate()
                .alpha(in ? 1f : 0f)
                .setDuration(FLYOUT_FADE_DURATION)
                .setInterpolator(in ? ALPHA_IN : ALPHA_OUT);
        animate()
                .translationY(in ? mFlyoutY : mFlyoutY - FLYOUT_FADE_Y)
                .setDuration(FLYOUT_FADE_DURATION)
                .setInterpolator(in ? ALPHA_IN : ALPHA_OUT);
    }

    private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) {
        final Drawable senderAvatar = flyoutMessage.senderAvatar;
        if (senderAvatar != null && flyoutMessage.isGroupChat) {
            mSenderAvatar.setVisibility(VISIBLE);
@@ -256,6 +281,27 @@ public class BubbleFlyoutView extends FrameLayout {
            mSenderText.setVisibility(GONE);
        }

        // 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
        // TextView being the desired width + extra padding).
        mMessageText.setMaxWidth(maxTextViewWidth);
        mMessageText.setText(flyoutMessage.message);
    }

    /** Configures the flyout, collapsed into dot form. */
    void setupFlyoutStartingAsDot(
            Bubble.FlyoutMessage flyoutMessage,
            PointF stackPos,
            float parentWidth,
            boolean arrowPointingLeft,
            int dotColor,
            @Nullable Runnable onLayoutComplete,
            @Nullable Runnable onHide,
            float[] dotCenter,
            boolean hideDot)  {

        updateFlyoutMessage(flyoutMessage, parentWidth);

        mArrowPointingLeft = arrowPointingLeft;
        mDotColor = dotColor;
        mOnHide = onHide;
@@ -263,24 +309,12 @@ public class BubbleFlyoutView extends FrameLayout {

        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
        // TextView being the desired width + extra padding).
        mMessageText.setMaxWidth(maxTextViewWidth);
        mMessageText.setText(flyoutMessage.message);

        // Wait for the TextView to lay out so we know its line count.
        // Wait for TextViews to layout with updated height.
        post(() -> {
            float restingTranslationY;
            // Multi line flyouts get top-aligned to the bubble.
            if (mMessageText.getLineCount() > 1) {
                restingTranslationY = stackPos.y + mBubbleIconTopPadding;
            } else {
                // Single line flyouts are vertically centered with respect to the bubble.
                restingTranslationY =
            // Flyout is vertically centered with respect to the bubble.
            mFlyoutY =
                    stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f;
            }
            setTranslationY(restingTranslationY);
            setTranslationY(mFlyoutY);

            // Calculate the translation required to position the flyout next to the bubble stack,
            // with the desired padding.
@@ -300,7 +334,7 @@ public class BubbleFlyoutView extends FrameLayout {
            final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway;

            final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX;
            final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY;
            final float distanceFromLayoutTopToDotCenterY = mFlyoutY - dotPositionY;

            mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX;
            mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY;
+30 −23
Original line number Diff line number Diff line
@@ -2196,11 +2196,7 @@ public class BubbleStackView extends FrameLayout
        return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop;
    }

    /**
     * Animates in the flyout for the given bubble, if available, and then hides it after some time.
     */
    @VisibleForTesting
    void animateInFlyoutForBubble(Bubble bubble) {
    private boolean shouldShowFlyout(Bubble bubble) {
        Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
        final BadgedImageView bubbleView = bubble.getIconView();
        if (flyoutMessage == null
@@ -2212,11 +2208,22 @@ public class BubbleStackView extends FrameLayout
                || mIsGestureInProgress
                || mBubbleToExpandAfterFlyoutCollapse != null
                || bubbleView == null) {
            if (bubbleView != null) {
            if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) {
                bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
            }
            // 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, or if bubble view is null.
            return false;
        }
        return true;
    }

    /**
     * Animates in the flyout for the given bubble, if available, and then hides it after some time.
     */
    @VisibleForTesting
    void animateInFlyoutForBubble(Bubble bubble) {
        if (!shouldShowFlyout(bubble)) {
            return;
        }

@@ -2234,25 +2241,22 @@ public class BubbleStackView extends FrameLayout
            }

            // Stop suppressing the dot now that the flyout has morphed into the dot.
            bubbleView.removeDotSuppressionFlag(
            bubble.getIconView().removeDotSuppressionFlag(
                    BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);

            mFlyout.setVisibility(INVISIBLE);

            // Hide the stack after a delay, if needed.
            updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
        };
        mFlyout.setVisibility(INVISIBLE);

        // Suppress the dot when we are animating the flyout.
        bubbleView.addDotSuppressionFlag(
        bubble.getIconView().addDotSuppressionFlag(
                BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);

        // Start flyout expansion. 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
            // layout.
            if (isExpanded()) {
            if (isExpanded() || bubble.getIconView() == null) {
                return;
            }
            final Runnable expandFlyoutAfterDelay = () -> {
@@ -2269,11 +2273,13 @@ public class BubbleStackView extends FrameLayout
                mFlyout.postDelayed(mAnimateInFlyout, 200);
            };

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

            mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
            if (mFlyout.getVisibility() == View.VISIBLE) {
                mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(),
                        mStackAnimationController.getStackPosition().y);
            } else {
                mFlyout.setVisibility(INVISIBLE);
                mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(),
                        mStackAnimationController.getStackPosition(), getWidth(),
                        mStackAnimationController.isStackOnLeftSide(),
                        bubble.getIconView().getDotColor() /* dotColor */,
@@ -2281,6 +2287,7 @@ public class BubbleStackView extends FrameLayout
                        mAfterFlyoutHidden,
                        bubble.getIconView().getDotCenter(),
                        !bubble.showDot());
            }
            mFlyout.bringToFront();
        });
        mFlyout.removeCallbacks(mHideFlyout);