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

Commit 65777105 authored by Johannes Gallmann's avatar Johannes Gallmann
Browse files

Polish RemoteInputView defocus animation

This CL polishes the defocus animation of the RemoteInputView:
1. The action container (containing "Reply"-button etc.) is cross faded with the RemoteInputView.
2. Some animator start delays are adjusted.
3. The animate flag is activated in the reset() function. The reset() function is called e.g. when a text message is sent and the RemoteInputView disappears. Some code restructuring was necessary for this change because parts of the reset() function need to be executed after the animation is finished.

All three changes are added behind the already existing inline-reply-animation flag.

Bug: 174148449
Test: atest RemoteInputViewTest, Manual, e.g. analyzing screen recordings (together with Dimitri) of defocus animations after text messages are sent.
Change-Id: Ifc128917c1277908c2147abaf1669432d0249edb
parent e076cf2d
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -65,8 +65,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.util.DumpUtilsKt;
import com.android.systemui.util.ListenerSet;

import dagger.Lazy;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -74,6 +72,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;

import dagger.Lazy;

/**
 * Class for handling remote input state over a set of notifications. This class handles things
 * like keeping notifications temporarily that were cancelled as a response to a remote input
@@ -465,8 +465,7 @@ public class NotificationRemoteInputManager implements Dumpable {
        riv.getController().setRemoteInput(input);
        riv.getController().setRemoteInputs(inputs);
        riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
        ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
        riv.focusAnimated(parent);
        riv.focusAnimated();
        if (userMessageContent != null) {
            riv.setEditTextContent(userMessageContent);
        }
+77 −38
Original line number Diff line number Diff line
@@ -109,6 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
    private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
    private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
    private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120;
    private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180;

    public final Object mToken = new Object();

@@ -421,7 +423,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    }

    @VisibleForTesting
    void onDefocus(boolean animate, boolean logClose) {
    void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) {
        mController.removeRemoteInput(mEntry, mToken);
        mEntry.remoteInputText = mEditText.getText();

@@ -431,18 +433,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
            ViewGroup parent = (ViewGroup) getParent();
            if (animate && parent != null && mIsFocusAnimationFlagActive) {


                ViewGroup grandParent = (ViewGroup) parent.getParent();
                ViewGroupOverlay overlay = parent.getOverlay();
                View actionsContainer = getActionsContainerLayout();
                int actionsContainerHeight =
                        actionsContainer != null ? actionsContainer.getHeight() : 0;

                // After adding this RemoteInputView to the overlay of the parent (and thus removing
                // it from the parent itself), the parent will shrink in height. This causes the
                // overlay to be moved. To correct the position of the overlay we need to offset it.
                int overlayOffsetY = getMaxSiblingHeight() - getHeight();
                int overlayOffsetY = actionsContainerHeight - getHeight();
                overlay.add(this);
                if (grandParent != null) grandParent.setClipChildren(false);

                Animator animator = getDefocusAnimator(overlayOffsetY);
                Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY);
                View self = this;
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
@@ -454,8 +458,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
                        if (mWrapper != null) {
                            mWrapper.setRemoteInputVisible(false);
                        }
                        if (doAfterDefocus != null) {
                            doAfterDefocus.run();
                        }
                    }
                });
                if (actionsContainer != null) actionsContainer.setAlpha(0f);
                animator.start();

            } else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
@@ -474,6 +482,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
                reveal.start();
            } else {
                setVisibility(GONE);
                if (doAfterDefocus != null) doAfterDefocus.run();
                if (mWrapper != null) {
                    mWrapper.setRemoteInputVisible(false);
                }
@@ -596,10 +605,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene

    /**
     * Focuses the RemoteInputView and animates its appearance
     *
     * @param crossFadeView view that will be crossfaded during the appearance animation
     */
    public void focusAnimated(View crossFadeView) {
    public void focusAnimated() {
        if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
                && mRevealParams != null) {
            android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
@@ -609,7 +616,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
            mIsAnimatingAppearance = true;
            setAlpha(0f);
            Animator focusAnimator = getFocusAnimator(crossFadeView);
            Animator focusAnimator = getFocusAnimator(getActionsContainerLayout());
            focusAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
@@ -661,6 +668,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    }

    private void reset() {
        if (mIsFocusAnimationFlagActive) {
            mProgressBar.setVisibility(INVISIBLE);
            mResetting = true;
            mSending = false;
            onDefocus(true /* animate */, false /* logClose */, () -> {
                mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
                mEditText.getText().clear();
                mEditText.setEnabled(isAggregatedVisible());
                mSendButton.setVisibility(VISIBLE);
                mController.removeSpinning(mEntry.getKey(), mToken);
                updateSendButton();
                setAttachment(null);
                mResetting = false;
            });
            return;
        }

        mResetting = true;
        mSending = false;
        mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
@@ -671,7 +695,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        mProgressBar.setVisibility(INVISIBLE);
        mController.removeSpinning(mEntry.getKey(), mToken);
        updateSendButton();
        onDefocus(false /* animate */, false /* logClose */);
        onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */);
        setAttachment(null);

        mResetting = false;
@@ -825,23 +849,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
    }

    /**
     * @return max sibling height (0 in case of no siblings)
     * @return action button container view (i.e. ViewGroup containing Reply button etc.)
     */
    public int getMaxSiblingHeight() {
    public View getActionsContainerLayout() {
        ViewGroup parentView = (ViewGroup) getParent();
        int maxHeight = 0;
        if (parentView == null) return 0;
        for (int i = 0; i < parentView.getChildCount(); i++) {
            View siblingView = parentView.getChildAt(i);
            if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
        }
        return maxHeight;
        if (parentView == null) return null;
        return parentView.findViewById(com.android.internal.R.id.actions_container_layout);
    }

    /**
     * Creates an animator for the focus animation.
     *
     * @param fadeOutView View that will be faded out during the focus animation.
     */
    private Animator getFocusAnimator(View crossFadeView) {
    private Animator getFocusAnimator(@Nullable View fadeOutView) {
        final AnimatorSet animatorSet = new AnimatorSet();

        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
        alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
        alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
@@ -854,30 +877,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
        scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
        scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);

        final Animator crossFadeViewAlphaAnimator =
                ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f);
        crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
        crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
        alphaAnimator.addListener(new AnimatorListenerAdapter() {
        if (fadeOutView == null) {
            animatorSet.playTogether(alphaAnimator, scaleAnimator);
        } else {
            final Animator fadeOutViewAlphaAnimator =
                    ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f);
            fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
            fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                crossFadeView.setAlpha(1f);
                    fadeOutView.setAlpha(1f);
                }
            });

        final AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator);
            animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator);
        }
        return animatorSet;
    }

    /**
     * Creates an animator for the defocus animation.
     *
     * @param fadeInView View that will be faded in during the defocus animation.
     * @param offsetY    The RemoteInputView will be offset by offsetY during the animation
     */
    private Animator getDefocusAnimator(int offsetY) {
    private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) {
        final AnimatorSet animatorSet = new AnimatorSet();

        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
        alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
        alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
        alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY);
        alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);

        ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
@@ -893,8 +922,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
            }
        });

        final AnimatorSet animatorSet = new AnimatorSet();
        if (fadeInView == null) {
            animatorSet.playTogether(alphaAnimator, scaleAnimator);
        } else {
            fadeInView.forceHasOverlappingRendering(false);
            Animator fadeInViewAlphaAnimator =
                    ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f);
            fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
            fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
            fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY);
            animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator);
        }
        return animatorSet;
    }

@@ -1011,7 +1049,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
            if (isFocusable() && isEnabled()) {
                setInnerFocusable(false);
                if (mRemoteInputView != null) {
                    mRemoteInputView.onDefocus(animate, true /* logClose */);
                    mRemoteInputView
                            .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */);
                }
                mShowImeOnInputConnection = false;
            }
+24 −9
Original line number Diff line number Diff line
@@ -390,19 +390,27 @@ public class RemoteInputViewTest extends SysuiTestCase {
        bindController(view, row.getEntry());
        view.setVisibility(View.GONE);

        View crossFadeView = new View(mContext);
        View fadeOutView = new View(mContext);
        fadeOutView.setId(com.android.internal.R.id.actions_container_layout);

        // Start focus animation
        view.focusAnimated(crossFadeView);
        FrameLayout parent = new FrameLayout(mContext);
        parent.addView(view);
        parent.addView(fadeOutView);

        // Start focus animation
        view.focusAnimated();
        assertTrue(view.isAnimatingAppearance());

        // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f
        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1);
        assertEquals(0f, fadeOutView.getAlpha());

        // fast forward to end of animation
        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
        mAnimatorTestRule.advanceTimeBy(1);

        // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind
        // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind
        // RemoteInputView)
        assertEquals(1f, crossFadeView.getAlpha());
        assertEquals(1f, fadeOutView.getAlpha());
        assertFalse(view.isAnimatingAppearance());
        assertEquals(View.VISIBLE, view.getVisibility());
        assertEquals(1f, view.getAlpha());
@@ -415,20 +423,27 @@ public class RemoteInputViewTest extends SysuiTestCase {
                mDependency,
                TestableLooper.get(this));
        ExpandableNotificationRow row = helper.createRow();
        FrameLayout remoteInputViewParent = new FrameLayout(mContext);
        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
        remoteInputViewParent.addView(view);
        bindController(view, row.getEntry());

        View fadeInView = new View(mContext);
        fadeInView.setId(com.android.internal.R.id.actions_container_layout);

        FrameLayout parent = new FrameLayout(mContext);
        parent.addView(view);
        parent.addView(fadeInView);

        // Start defocus animation
        view.onDefocus(true, false);
        view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */);
        assertEquals(View.VISIBLE, view.getVisibility());
        assertEquals(0f, fadeInView.getAlpha());

        // fast forward to end of animation
        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);

        // assert that RemoteInputView is no longer visible
        assertEquals(View.GONE, view.getVisibility());
        assertEquals(1f, fadeInView.getAlpha());
    }

    // NOTE: because we're refactoring the RemoteInputView and moving logic into the