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

Commit 93d35ce8 authored by Johannes Gallmann's avatar Johannes Gallmann Committed by Android (Google) Code Review
Browse files

Merge "Predictive IME back animation" into main

parents 00b9058b 3c183b4a
Loading
Loading
Loading
Loading
+18 −8
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.inputmethodservice;

import static android.view.inputmethod.Flags.predictiveBackIme;
import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VIEW_STARTED;
import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VISIBILITY;
import static android.inputmethodservice.InputMethodServiceProto.CONFIGURATION;
@@ -3098,7 +3099,7 @@ public class InputMethodService extends AbstractInputMethodService {
        cancelImeSurfaceRemoval();
        mInShowWindow = false;
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
        registerCompatOnBackInvokedCallback();
        registerDefaultOnBackInvokedCallback();
    }


@@ -3107,18 +3108,27 @@ public class InputMethodService extends AbstractInputMethodService {
     *  back dispatching is enabled. We keep the {@link KeyEvent#KEYCODE_BACK} based legacy code
     *  around to handle back on older devices.
     */
    private void registerCompatOnBackInvokedCallback() {
    private void registerDefaultOnBackInvokedCallback() {
        if (mBackCallbackRegistered) {
            return;
        }
        if (mWindow != null) {
            if (getApplicationInfo().isOnBackInvokedCallbackEnabled() && predictiveBackIme()) {
                // Register the compat callback as system-callback if IME has opted in for
                // predictive back (and predictiveBackIme feature flag is enabled). This indicates
                // to the receiving process (application process) that a predictive IME dismiss
                // animation may be played instead of invoking the callback.
                mWindow.getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(
                        mCompatBackCallback);
            } else {
                mWindow.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                        OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatBackCallback);
            }
            mBackCallbackRegistered = true;
        }
    }

    private void unregisterCompatOnBackInvokedCallback() {
    private void unregisterDefaultOnBackInvokedCallback() {
        if (!mBackCallbackRegistered) {
            return;
        }
@@ -3251,7 +3261,7 @@ public class InputMethodService extends AbstractInputMethodService {
        }
        mLastWasInFullscreenMode = mIsFullscreen;
        updateFullscreenMode();
        unregisterCompatOnBackInvokedCallback();
        unregisterDefaultOnBackInvokedCallback();
    }

    /**
@@ -3328,7 +3338,7 @@ public class InputMethodService extends AbstractInputMethodService {
        // Back callback is typically unregistered in {@link #hideWindow()}, but it's possible
        // for {@link #doFinishInput()} to be called without {@link #hideWindow()} so we also
        // unregister here.
        unregisterCompatOnBackInvokedCallback();
        unregisterDefaultOnBackInvokedCallback();
    }

    void doStartInput(InputConnection ic, EditorInfo editorInfo, boolean restarting) {
@@ -4473,7 +4483,7 @@ public class InputMethodService extends AbstractInputMethodService {
    private void compatHandleBack() {
        if (!mDecorViewVisible) {
            Log.e(TAG, "Back callback invoked on a hidden IME. Removing the callback...");
            unregisterCompatOnBackInvokedCallback();
            unregisterDefaultOnBackInvokedCallback();
            return;
        }
        final KeyEvent downEvent = createBackKeyEvent(
+220 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view;

import static android.view.InsetsController.ANIMATION_TYPE_USER;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
import android.util.Log;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;

/**
 * Controller for IME predictive back animation
 *
 * @hide
 */
public class ImeBackAnimationController implements OnBackAnimationCallback {

    private static final String TAG = "ImeBackAnimationController";
    private static final int POST_COMMIT_DURATION_MS = 200;
    private static final int POST_COMMIT_CANCEL_DURATION_MS = 50;
    private static final float PEEK_FRACTION = 0.1f;
    private static final Interpolator STANDARD_DECELERATE = new PathInterpolator(0f, 0f, 0f, 1f);
    private static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
            0.05f, 0.7f, 0.1f, 1f);
    private static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(0.3f, 0f, 1f, 1f);

    private final InsetsController mInsetsController;
    private final ViewRootImpl mViewRoot;
    private WindowInsetsAnimationController mWindowInsetsAnimationController = null;
    private ValueAnimator mPostCommitAnimator = null;
    private float mLastProgress = 0f;
    private boolean mTriggerBack = false;
    private boolean mIsPreCommitAnimationInProgress = false;

    public ImeBackAnimationController(ViewRootImpl viewRoot) {
        mInsetsController = viewRoot.getInsetsController();
        mViewRoot = viewRoot;
    }

    @Override
    public void onBackStarted(@NonNull BackEvent backEvent) {
        if (isAdjustResize()) {
            // There is no good solution for a predictive back animation if the app uses
            // adjustResize, since we can't relayout the whole app for every frame. We also don't
            // want to reveal any black areas behind the IME. Therefore let's not play any animation
            // in that case for now.
            Log.d(TAG, "onBackStarted -> not playing predictive back animation due to softinput"
                    + " mode adjustResize");
            return;
        }
        if (isHideAnimationInProgress()) {
            // If IME is currently animating away, skip back gesture
            return;
        }
        mIsPreCommitAnimationInProgress = true;
        if (mWindowInsetsAnimationController != null) {
            // There's still an active animation controller. This means that a cancel post commit
            // animation of an earlier back gesture is still in progress. Let's cancel it and let
            // the new gesture seamlessly take over.
            resetPostCommitAnimator();
            setPreCommitProgress(0f);
            return;
        }
        mInsetsController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null,
                new WindowInsetsAnimationControlListener() {
                    @Override
                    public void onReady(@NonNull WindowInsetsAnimationController controller,
                            @WindowInsets.Type.InsetsType int types) {
                        mWindowInsetsAnimationController = controller;
                        if (mIsPreCommitAnimationInProgress) {
                            setPreCommitProgress(mLastProgress);
                        } else {
                            // gesture has already finished before IME became ready to animate
                            startPostCommitAnim(mTriggerBack);
                        }
                    }

                    @Override
                    public void onFinished(@NonNull WindowInsetsAnimationController controller) {
                        reset();
                    }

                    @Override
                    public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
                        reset();
                    }
                }, /*fromIme*/ false, /*durationMs*/ -1, /*interpolator*/ null, ANIMATION_TYPE_USER,
                /*fromPredictiveBack*/ true);
    }

    @Override
    public void onBackProgressed(@NonNull BackEvent backEvent) {
        mLastProgress = backEvent.getProgress();
        setPreCommitProgress(mLastProgress);
    }

    @Override
    public void onBackCancelled() {
        if (isAdjustResize()) return;
        startPostCommitAnim(/*hideIme*/ false);
    }

    @Override
    public void onBackInvoked() {
        if (isAdjustResize()) {
            mInsetsController.hide(ime());
            return;
        }
        startPostCommitAnim(/*hideIme*/ true);
    }

    private void setPreCommitProgress(float progress) {
        if (isHideAnimationInProgress()) return;
        if (mWindowInsetsAnimationController != null) {
            float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom;
            float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom;
            float imeHeight = shownY - hiddenY;
            float interpolatedProgress = STANDARD_DECELERATE.getInterpolation(progress);
            int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION));
            mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f,
                    progress);
        }
    }

    private void startPostCommitAnim(boolean triggerBack) {
        mIsPreCommitAnimationInProgress = false;
        if (mWindowInsetsAnimationController == null || isHideAnimationInProgress()) {
            mTriggerBack = triggerBack;
            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);
        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());
            } else {
                reset();
            }
        });
        mPostCommitAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animator) {
                if (mIsPreCommitAnimationInProgress) {
                    // this means a new gesture has started while the cancel-post-commit-animation
                    // was in progress. Let's not reset anything and let the new user gesture take
                    // over seamlessly
                    return;
                }
                if (mWindowInsetsAnimationController != null) {
                    mWindowInsetsAnimationController.finish(!triggerBack);
                }
                reset();
            }
        });
        mPostCommitAnimator.setDuration(
                triggerBack ? POST_COMMIT_DURATION_MS : POST_COMMIT_CANCEL_DURATION_MS);
        mPostCommitAnimator.start();
    }

    private void reset() {
        mWindowInsetsAnimationController = null;
        resetPostCommitAnimator();
        mLastProgress = 0f;
        mTriggerBack = false;
        mIsPreCommitAnimationInProgress = false;
    }

    private void resetPostCommitAnimator() {
        if (mPostCommitAnimator != null) {
            mPostCommitAnimator.cancel();
            mPostCommitAnimator = null;
        }
    }

    private boolean isAdjustResize() {
        return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST)
                == SOFT_INPUT_ADJUST_RESIZE;
    }

    private boolean isHideAnimationInProgress() {
        return mPostCommitAnimator != null && mTriggerBack;
    }

}
+17 −8
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ import static android.view.WindowInsets.Type.all;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;

import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -322,7 +324,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
    public static final int ANIMATION_TYPE_HIDE = 1;

    /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */
    @VisibleForTesting
    @VisibleForTesting(visibility = PACKAGE)
    public static final int ANIMATION_TYPE_USER = 2;

    /** Running animation will resize insets */
@@ -1076,7 +1078,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        show(types, false /* fromIme */, null /* statsToken */);
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    @VisibleForTesting(visibility = PACKAGE)
    public void show(@InsetsType int types, boolean fromIme,
            @Nullable ImeTracker.Token statsToken) {
        if ((types & ime()) != 0) {
@@ -1260,14 +1262,15 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
            @Nullable CancellationSignal cancellationSignal,
            @NonNull WindowInsetsAnimationControlListener listener) {
        controlWindowInsetsAnimation(types, cancellationSignal, listener,
                false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER);
                false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER,
                false /* fromPredictiveBack */);
    }

    private void controlWindowInsetsAnimation(@InsetsType int types,
    void controlWindowInsetsAnimation(@InsetsType int types,
            @Nullable CancellationSignal cancellationSignal,
            WindowInsetsAnimationControlListener listener,
            boolean fromIme, long durationMs, @Nullable Interpolator interpolator,
            @AnimationType int animationType) {
            @AnimationType int animationType, boolean fromPredictiveBack) {
        if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) {
            listener.onCancelled(null);
            return;
@@ -1279,7 +1282,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        }

        controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
                interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
                interpolator, animationType,
                getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
                false /* useInsetsAnimationThread */, null /* statsToken */);
    }

@@ -1526,7 +1530,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
    }

    private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
            @InsetsType int types) {
            @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
            return LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
        }
        // Generally, we want to layout the opposite of the current state. This is to make animation
        // callbacks easy to use: The can capture the layout values and then treat that as end-state
        // during the animation.
@@ -1730,7 +1739,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        return ANIMATION_TYPE_NONE;
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    @VisibleForTesting(visibility = PACKAGE)
    public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
        final @InsetsType int requestedVisibleTypes =
                (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
+6 −2
Original line number Diff line number Diff line
@@ -115,6 +115,7 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.window.flags.Flags.activityWindowInfoFlag;
import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
import static com.android.window.flags.Flags.setScPropertiesInClient;
@@ -942,6 +943,7 @@ public final class ViewRootImpl implements ViewParent,
                    new InputEventConsistencyVerifier(this, 0) : null;
    private final InsetsController mInsetsController;
    private final ImeBackAnimationController mImeBackAnimationController;
    private final ImeFocusController mImeFocusController;
    private boolean mIsSurfaceOpaque;
@@ -1206,6 +1208,7 @@ public final class ViewRootImpl implements ViewParent,
        // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
        mChoreographer = Choreographer.getInstance();
        mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
        mImeBackAnimationController = new ImeBackAnimationController(this);
        mHandwritingInitiator = new HandwritingInitiator(
                mViewConfiguration,
                mContext.getSystemService(InputMethodManager.class));
@@ -3181,7 +3184,7 @@ public final class ViewRootImpl implements ViewParent,
                        == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    }
    @VisibleForTesting
    @VisibleForTesting(visibility = PACKAGE)
    public InsetsController getInsetsController() {
        return mInsetsController;
    }
@@ -12148,7 +12151,8 @@ public final class ViewRootImpl implements ViewParent,
                            + "IWindow:%s Session:%s",
                    mOnBackInvokedDispatcher, mBasePackageName, mWindow, mWindowSession));
        }
        mOnBackInvokedDispatcher.attachToWindow(mWindowSession, mWindow);
        mOnBackInvokedDispatcher.attachToWindow(mWindowSession, mWindow,
                mImeBackAnimationController);
    }
    private void sendBackKeyEvent(int action) {
+22 −2
Original line number Diff line number Diff line
@@ -148,8 +148,17 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
            @OnBackInvokedDispatcher.Priority int priority,
            int callbackId,
            @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
        final ImeOnBackInvokedCallback imeCallback =
                new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
        final ImeOnBackInvokedCallback imeCallback;
        if (priority == PRIORITY_SYSTEM) {
            // A callback registration with PRIORITY_SYSTEM indicates that a predictive back
            // animation can be played on the IME. Therefore register the
            // DefaultImeOnBackInvokedCallback with the receiving dispatcher and override the
            // priority to PRIORITY_DEFAULT.
            priority = PRIORITY_DEFAULT;
            imeCallback = new DefaultImeOnBackAnimationCallback(iCallback, callbackId, priority);
        } else {
            imeCallback = new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
        }
        mImeCallbacks.add(imeCallback);
        receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority);
    }
@@ -229,6 +238,17 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc
        }
    }

    /**
     * Subclass of ImeOnBackInvokedCallback indicating that a predictive IME back animation may be
     * played instead of invoking the callback.
     */
    static class DefaultImeOnBackAnimationCallback extends ImeOnBackInvokedCallback {
        DefaultImeOnBackAnimationCallback(@NonNull IOnBackInvokedCallback iCallback, int id,
                int priority) {
            super(iCallback, id, priority);
        }
    }

    /**
     * Transfers {@link ImeOnBackInvokedCallback}s registered on one {@link ViewRootImpl} to
     * another {@link ViewRootImpl} on focus change.
Loading