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

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

Prevent insets switch during IME predictive back animation...

...when app has animation callback registered

This CL changes an important aspect of the IME predictive back
animation: When the app has an animation callback registered, insets are
now HIDDEN from the start of the predictive back animation, instead of
remaining as SHOWN during the precommit phase and switching to HIDDEN as
soon as the back gesture is committed.

The reason for this change is to be compatible with apps that are
following the official documentation at
https://developer.android.com/develop/ui/views/layout/sw-keyboard to
animate app content in sync with the IME. The official documentation
states that insets always switch at the beginning of the animation
(between onPrepare and onStart of WindowInsetsAnimation.Callback). This
needs to be valid for IME predictive back too, otherwise some apps
animation logic might break.

Here's a brief explanation of the changes in this CL:
1. The custom handling for ime-predictive-back in
InsetsController#getLayoutInsetsDuringAnimationMode is only applied if
there is no animation callback registered.
2. We must prevent ViewRootImpl from moving app content back to it's
original position at the start of the back gesture in the adjust_pan
case. The pan is controlled by ImeBackAnimationController instead.

When the refactor_insets_controller flag is enabled, these two changes
would already be sufficient. For the case of that flag not being
enabled, two additional targeted fixes are necessary:
1. In InsetsController#collectSourceControls, we want to treat the
ime-predictive-back case (with animation-callback) as a hide request
instead of a show request. Without that change, starting a
predictive-back gesture while the IME is still animating in doesn't work
properly.
2. In ImeInsetsSourceConsumer it needs to be ensured that the insets are
reported as changed after a predictive back gesture is cancelled and the
IME was animated back to its shown position. Without that change, there
would be a flicker in that case.

Bug: 359175310
Test: Manual, i.e. extensive manual testing with refactor_insets_controller flag enabled and disabled in different apps (ChatActivity test app, Instagram, Launcher all apps, Settings, Google Keep etc.) and various use cases (animation interruptions, quick refocusing, quick rehiding etc.)
Test: ImeBackAnimationControllerTest, InsetsControllerTest
Flag: android.view.inputmethod.predictive_back_ime
Change-Id: I288c7acadb12f84394f8a64b3b0a7898fde3ab7e
parent eeae7591
Loading
Loading
Loading
Loading
+14 −21
Original line number Diff line number Diff line
@@ -149,15 +149,17 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {

    private void setPreCommitProgress(float progress) {
        if (isHideAnimationInProgress()) return;
        setInterpolatedProgress(BACK_GESTURE.getInterpolation(progress) * PEEK_FRACTION);
    }

    private void setInterpolatedProgress(float progress) {
        if (mWindowInsetsAnimationController != null) {
            float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom;
            float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom;
            float imeHeight = shownY - hiddenY;
            float interpolatedProgress = BACK_GESTURE.getInterpolation(progress);
            int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION));
            int newY = (int) (imeHeight - progress * imeHeight);
            if (mStartRootScrollY != 0) {
                mViewRoot.setScrollY(
                        (int) (mStartRootScrollY * (1 - interpolatedProgress * PEEK_FRACTION)));
                mViewRoot.setScrollY((int) (mStartRootScrollY * (1 - progress)));
            }
            mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f,
                    progress);
@@ -171,21 +173,14 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
            return;
        }
        mTriggerBack = triggerBack;
        int currentBottomInset = mWindowInsetsAnimationController.getCurrentInsets().bottom;
        int targetBottomInset;
        if (triggerBack) {
            targetBottomInset = mWindowInsetsAnimationController.getHiddenStateInsets().bottom;
        } else {
            targetBottomInset = mWindowInsetsAnimationController.getShownStateInsets().bottom;
        }
        mPostCommitAnimator = ValueAnimator.ofFloat(currentBottomInset, targetBottomInset);
        float targetProgress = triggerBack ? 1f : 0f;
        mPostCommitAnimator = ValueAnimator.ofFloat(
                BACK_GESTURE.getInterpolation(mLastProgress) * PEEK_FRACTION, targetProgress);
        mPostCommitAnimator.setInterpolator(
                triggerBack ? STANDARD_ACCELERATE : EMPHASIZED_DECELERATE);
        mPostCommitAnimator.addUpdateListener(animation -> {
            int bottomInset = (int) ((float) animation.getAnimatedValue());
            if (mWindowInsetsAnimationController != null) {
                mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, bottomInset),
                        1f, animation.getAnimatedFraction());
                setInterpolatedProgress((float) animation.getAnimatedValue());
            } else {
                reset();
            }
@@ -213,14 +208,8 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
            notifyHideIme();
            // requesting IME as invisible during post-commit
            mInsetsController.setRequestedVisibleTypes(0, ime());
            // Changes the animation state. This also notifies RootView of changed insets, which
            // causes it to reset its scrollY to 0f (animated) if it was panned
            mInsetsController.onAnimationStateChanged(ime(), /*running*/ true);
        }
        if (mStartRootScrollY != 0 && !triggerBack) {
            // This causes RootView to update its scroll back to the panned position
            mInsetsController.getHost().notifyInsetsChanged();
        }
    }

    private void notifyHideIme() {
@@ -282,6 +271,10 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
        return mPostCommitAnimator != null && mTriggerBack;
    }

    boolean isAnimationInProgress() {
        return mIsPreCommitAnimationInProgress || mWindowInsetsAnimationController != null;
    }

    /**
     * Dump information about this ImeBackAnimationController
     *
+8 −1
Original line number Diff line number Diff line
@@ -70,7 +70,14 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer {
                        "ImeInsetsSourceConsumer#onAnimationFinished",
                        mController.getHost().getInputMethodManager(), null /* icProto */);
            }
            boolean insetsChanged = super.onAnimationStateChanged(running);
            boolean insetsChanged = false;
            if (Flags.predictiveBackIme() && !running && isShowRequested()
                    && mAnimationState == ANIMATION_STATE_HIDE) {
                // A user controlled hide animation may have ended in the shown state (e.g.
                // cancelled predictive back animation) -> Insets need to be reset to shown.
                insetsChanged |= applyLocalVisibilityOverride();
            }
            insetsChanged |= super.onAnimationStateChanged(running);
            if (running && !isShowRequested()
                    && mController.isPredictiveBackImeHideAnimInProgress()) {
                // IME predictive back animation switched from pre-commit to post-commit.
+20 −13
Original line number Diff line number Diff line
@@ -1197,7 +1197,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
                pendingRequest.listener, null /* frame */, true /* fromIme */,
                pendingRequest.mInsetsAnimationSpec,
                pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation,
                pendingRequest.useInsetsAnimationThread, statsToken);
                pendingRequest.useInsetsAnimationThread, statsToken,
                false /* fromPredictiveBack */);
    }

    @Override
@@ -1330,7 +1331,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        // TODO(b/342111149): Create statsToken here once ImeTracker#onStart becomes async.
        controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, spec,
                animationType, getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
                false /* useInsetsAnimationThread */, null);
                false /* useInsetsAnimationThread */, null, fromPredictiveBack);
    }

    private void controlAnimationUnchecked(@InsetsType int types,
@@ -1338,7 +1339,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
            WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
            InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
            @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken,
            boolean fromPredictiveBack) {
        final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN;

        // Basically, we accept the requested visibilities from the upstream callers...
@@ -1348,7 +1350,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        // rejecting showing IME.
        controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme,
                insetsAnimationSpec, animationType, layoutInsetsDuringAnimation,
                useInsetsAnimationThread, statsToken);
                useInsetsAnimationThread, statsToken, fromPredictiveBack);

        // We are finishing setting the requested visible types. Report them to the server
        // and/or the app.
@@ -1360,7 +1362,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
            WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
            InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
            @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken,
            boolean fromPredictiveBack) {
        if ((types & mTypesBeingCancelled) != 0) {
            final boolean monitoredAnimation =
                    animationType == ANIMATION_TYPE_SHOW || animationType == ANIMATION_TYPE_HIDE;
@@ -1446,7 +1449,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
            }
        } else {
            Pair<Integer, Boolean> typesReadyPair = collectSourceControls(
                    fromIme, types, controls, animationType, statsToken);
                    fromIme, types, controls, animationType, statsToken, fromPredictiveBack);
            typesReady = typesReadyPair.first;
            boolean imeReady = typesReadyPair.second;
            if (DEBUG) {
@@ -1582,7 +1585,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
     */
    private Pair<Integer, Boolean> collectSourceControls(boolean fromIme, @InsetsType int types,
            SparseArray<InsetsSourceControl> controls, @AnimationType int animationType,
            @Nullable ImeTracker.Token statsToken) {
            @Nullable ImeTracker.Token statsToken, boolean fromPredictiveBack) {
        ImeTracker.forLogging().onProgress(statsToken,
                ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS);

@@ -1594,7 +1597,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
                continue;
            }
            boolean show = animationType == ANIMATION_TYPE_SHOW
                    || animationType == ANIMATION_TYPE_USER;
                    || (animationType == ANIMATION_TYPE_USER
                            && (!fromPredictiveBack || !mHost.hasAnimationCallbacks()));
            boolean canRun = true;
            if (show) {
                // Show request
@@ -1617,7 +1621,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
                        break;
                }
            } else {
                consumer.requestHide(fromIme, statsToken);
                consumer.requestHide(fromIme
                        || (fromPredictiveBack && mHost.hasAnimationCallbacks()), statsToken);
            }
            if (!canRun) {
                if (WARN) Log.w(TAG, String.format(
@@ -1672,9 +1677,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation

    private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
            @InsetsType int types, boolean fromPredictiveBack) {
        if (fromPredictiveBack) {
            // When insets are animated by predictive back, we want insets to be shown to prevent a
            // jump cut from shown to hidden at the start of the predictive back animation
        if (fromPredictiveBack && !mHost.hasAnimationCallbacks()) {
            // When insets are animated by predictive back and the app does not have an animation
            // callback, we want insets to be shown to prevent a jump cut from shown to hidden at
            // the start of the predictive back animation
            return LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
        }
        // Generally, we want to layout the opposite of the current state. This is to make animation
@@ -2021,7 +2027,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
                listener /* insetsAnimationSpec */,
                show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
                show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
                !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
                !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken,
                false /* fromPredictiveBack */);
    }

    /**
+6 −0
Original line number Diff line number Diff line
@@ -6101,6 +6101,12 @@ public final class ViewRootImpl implements ViewParent,
    }
    boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
        if (mImeBackAnimationController.isAnimationInProgress()) {
            // IME predictive back animation is currently in progress which means that scrollY is
            // currently controlled by ImeBackAnimationController.
            return false;
        }
        final Rect ci = mAttachInfo.mContentInsets;
        final Rect vi = mAttachInfo.mVisibleInsets;
        int scrollY = 0;
+10 −7
Original line number Diff line number Diff line
@@ -254,11 +254,8 @@ public class ImeBackAnimationControllerTest {
        float progress = 0.5f;
        mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
        // verify correct ime insets manipulation
        float interpolatedProgress = BACK_GESTURE.getInterpolation(progress);
        int expectedInset =
                (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT);
        verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
                eq(Insets.of(0, 0, 0, expectedInset)), eq(1f), anyFloat());
                eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat());
    }

    @Test
@@ -268,12 +265,13 @@ public class ImeBackAnimationControllerTest {
            WindowInsetsAnimationControlListener animationControlListener = startBackGesture();

            // progress back gesture
            mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
            float progress = 0.5f;
            mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));

            // commit back gesture
            mBackAnimationController.onBackInvoked();

            // verify setInsetsAndAlpha never called due onReady delayed
            // verify setInsetsAndAlpha never called due to onReady delayed
            verify(mWindowInsetsAnimationController, never()).setInsetsAndAlpha(any(), anyInt(),
                    anyFloat());
            verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true));
@@ -283,7 +281,7 @@ public class ImeBackAnimationControllerTest {

            // verify setInsetsAndAlpha immediately called
            verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
                    eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
                    eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat());
            // verify post-commit hide anim has started
            verify(mInsetsController, times(1)).setPredictiveBackImeHideAnimInProgress(eq(true));
        });
@@ -319,4 +317,9 @@ public class ImeBackAnimationControllerTest {

        return animationControlListener.getValue();
    }

    private int getImeHeight(float gestureProgress) {
        float interpolatedProgress = BACK_GESTURE.getInterpolation(gestureProgress);
        return (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT);
    }
}
Loading