Loading core/java/android/inputmethodservice/InputMethodService.java +18 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -3098,7 +3099,7 @@ public class InputMethodService extends AbstractInputMethodService { cancelImeSurfaceRemoval(); mInShowWindow = false; Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); registerCompatOnBackInvokedCallback(); registerDefaultOnBackInvokedCallback(); } Loading @@ -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; } Loading Loading @@ -3251,7 +3261,7 @@ public class InputMethodService extends AbstractInputMethodService { } mLastWasInFullscreenMode = mIsFullscreen; updateFullscreenMode(); unregisterCompatOnBackInvokedCallback(); unregisterDefaultOnBackInvokedCallback(); } /** Loading Loading @@ -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) { Loading Loading @@ -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( Loading core/java/android/view/ImeBackAnimationController.java 0 → 100644 +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; } } core/java/android/view/InsetsController.java +17 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 */ Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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 */); } Loading Loading @@ -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. Loading Loading @@ -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); Loading core/java/android/view/ViewRootImpl.java +6 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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)); Loading Loading @@ -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; } Loading Loading @@ -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) { Loading core/java/android/window/ImeOnBackInvokedDispatcher.java +22 −2 Original line number Diff line number Diff line Loading @@ -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); } Loading Loading @@ -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 Loading
core/java/android/inputmethodservice/InputMethodService.java +18 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -3098,7 +3099,7 @@ public class InputMethodService extends AbstractInputMethodService { cancelImeSurfaceRemoval(); mInShowWindow = false; Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); registerCompatOnBackInvokedCallback(); registerDefaultOnBackInvokedCallback(); } Loading @@ -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; } Loading Loading @@ -3251,7 +3261,7 @@ public class InputMethodService extends AbstractInputMethodService { } mLastWasInFullscreenMode = mIsFullscreen; updateFullscreenMode(); unregisterCompatOnBackInvokedCallback(); unregisterDefaultOnBackInvokedCallback(); } /** Loading Loading @@ -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) { Loading Loading @@ -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( Loading
core/java/android/view/ImeBackAnimationController.java 0 → 100644 +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; } }
core/java/android/view/InsetsController.java +17 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 */ Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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 */); } Loading Loading @@ -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. Loading Loading @@ -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); Loading
core/java/android/view/ViewRootImpl.java +6 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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)); Loading Loading @@ -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; } Loading Loading @@ -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) { Loading
core/java/android/window/ImeOnBackInvokedDispatcher.java +22 −2 Original line number Diff line number Diff line Loading @@ -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); } Loading Loading @@ -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