1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import static android.view.InsetsController.ANIMATION_DURATION_SYNC_IME_MS; 20 import static android.view.InsetsController.ANIMATION_DURATION_UNSYNC_IME_MS; 21 import static android.view.InsetsController.ANIMATION_TYPE_USER; 22 import static android.view.InsetsController.FAST_OUT_LINEAR_IN_INTERPOLATOR; 23 import static android.view.InsetsController.SYNC_IME_INTERPOLATOR; 24 import static android.view.WindowInsets.Type.ime; 25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; 26 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 27 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorListenerAdapter; 31 import android.animation.ValueAnimator; 32 import android.annotation.NonNull; 33 import android.annotation.Nullable; 34 import android.app.WindowConfiguration; 35 import android.graphics.Insets; 36 import android.util.Log; 37 import android.view.animation.BackGestureInterpolator; 38 import android.view.animation.Interpolator; 39 import android.view.animation.PathInterpolator; 40 import android.view.inputmethod.Flags; 41 import android.view.inputmethod.ImeTracker; 42 import android.window.BackEvent; 43 import android.window.OnBackAnimationCallback; 44 45 import com.android.internal.inputmethod.SoftInputShowHideReason; 46 47 import java.io.PrintWriter; 48 49 /** 50 * Controller for IME predictive back animation 51 * 52 * @hide 53 */ 54 public class ImeBackAnimationController implements OnBackAnimationCallback { 55 56 private static final String TAG = "ImeBackAnimationController"; 57 private static final int POST_COMMIT_DURATION_MS = 200; 58 private static final int POST_COMMIT_CANCEL_DURATION_MS = 50; 59 private static final float PEEK_FRACTION = 0.1f; 60 private static final Interpolator BACK_GESTURE = new BackGestureInterpolator(); 61 private static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( 62 0.05f, 0.7f, 0.1f, 1f); 63 private final InsetsController mInsetsController; 64 private final ViewRootImpl mViewRoot; 65 private WindowInsetsAnimationController mWindowInsetsAnimationController = null; 66 private ValueAnimator mPostCommitAnimator = null; 67 private float mLastProgress = 0f; 68 private boolean mTriggerBack = false; 69 private boolean mIsPreCommitAnimationInProgress = false; 70 private int mStartRootScrollY = 0; 71 ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController)72 public ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController) { 73 mInsetsController = insetsController; 74 mViewRoot = viewRoot; 75 } 76 77 @Override onBackStarted(@onNull BackEvent backEvent)78 public void onBackStarted(@NonNull BackEvent backEvent) { 79 if (!isBackAnimationAllowed()) { 80 // There is no good solution for a predictive back animation if the app uses 81 // adjustResize, since we can't relayout the whole app for every frame. We also don't 82 // want to reveal any black areas behind the IME. Therefore let's not play any animation 83 // in that case for now. 84 Log.d(TAG, "onBackStarted -> not playing predictive back animation due to softinput" 85 + " mode adjustResize AND no animation callback registered"); 86 return; 87 } 88 if (isHideAnimationInProgress()) { 89 // If IME is currently animating away, skip back gesture 90 return; 91 } 92 mIsPreCommitAnimationInProgress = true; 93 if (mWindowInsetsAnimationController != null) { 94 // There's still an active animation controller. This means that a cancel post commit 95 // animation of an earlier back gesture is still in progress. Let's cancel it and let 96 // the new gesture seamlessly take over. 97 resetPostCommitAnimator(); 98 setPreCommitProgress(0f); 99 return; 100 } 101 mInsetsController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null, 102 new WindowInsetsAnimationControlListener() { 103 @Override 104 public void onReady(@NonNull WindowInsetsAnimationController controller, 105 @WindowInsets.Type.InsetsType int types) { 106 mWindowInsetsAnimationController = controller; 107 if (isAdjustPan()) mStartRootScrollY = mViewRoot.mScrollY; 108 if (mIsPreCommitAnimationInProgress) { 109 setPreCommitProgress(mLastProgress); 110 } else { 111 // gesture has already finished before IME became ready to animate 112 startPostCommitAnim(mTriggerBack); 113 } 114 } 115 116 @Override 117 public void onFinished(@NonNull WindowInsetsAnimationController controller) { 118 reset(); 119 } 120 121 @Override 122 public void onCancelled(@Nullable WindowInsetsAnimationController controller) { 123 reset(); 124 } 125 }, /*fromIme*/ false, /*durationMs*/ -1, /*interpolator*/ null, ANIMATION_TYPE_USER, 126 /*fromPredictiveBack*/ true); 127 } 128 129 @Override onBackProgressed(@onNull BackEvent backEvent)130 public void onBackProgressed(@NonNull BackEvent backEvent) { 131 mLastProgress = backEvent.getProgress(); 132 setPreCommitProgress(mLastProgress); 133 } 134 135 @Override onBackCancelled()136 public void onBackCancelled() { 137 if (!isBackAnimationAllowed()) return; 138 startPostCommitAnim(/*hideIme*/ false); 139 } 140 141 @Override onBackInvoked()142 public void onBackInvoked() { 143 if (!isBackAnimationAllowed() || !mIsPreCommitAnimationInProgress) { 144 // play regular hide animation if predictive back-animation is not allowed or if insets 145 // control has been cancelled by the system. This can happen in multi-window mode for 146 // example (i.e. split-screen or activity-embedding) 147 notifyHideIme(); 148 } else { 149 startPostCommitAnim(/*hideIme*/ true); 150 } 151 if (Flags.refactorInsetsController()) { 152 // Unregister all IME back callbacks so that back events are sent to the next callback 153 // even while the hide animation is playing 154 mInsetsController.getHost().getInputMethodManager().getImeOnBackInvokedDispatcher() 155 .preliminaryClear(); 156 } 157 } 158 setPreCommitProgress(float progress)159 private void setPreCommitProgress(float progress) { 160 if (isHideAnimationInProgress()) return; 161 setInterpolatedProgress(BACK_GESTURE.getInterpolation(progress) * PEEK_FRACTION); 162 } 163 setInterpolatedProgress(float progress)164 private void setInterpolatedProgress(float progress) { 165 if (mWindowInsetsAnimationController != null) { 166 float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; 167 float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom; 168 float imeHeight = shownY - hiddenY; 169 int newY = (int) (imeHeight - progress * imeHeight); 170 if (mStartRootScrollY != 0) { 171 mViewRoot.setScrollY((int) (mStartRootScrollY * (1 - progress))); 172 } 173 mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f, 174 progress); 175 } 176 } 177 startPostCommitAnim(boolean triggerBack)178 private void startPostCommitAnim(boolean triggerBack) { 179 mIsPreCommitAnimationInProgress = false; 180 if (mWindowInsetsAnimationController == null || isHideAnimationInProgress()) { 181 mTriggerBack = triggerBack; 182 return; 183 } 184 mTriggerBack = triggerBack; 185 float targetProgress = triggerBack ? 1f : 0f; 186 mPostCommitAnimator = ValueAnimator.ofFloat( 187 BACK_GESTURE.getInterpolation(mLastProgress) * PEEK_FRACTION, targetProgress); 188 Interpolator interpolator; 189 long duration; 190 if (triggerBack && mViewRoot.mView.hasWindowInsetsAnimationCallback() 191 && mWindowInsetsAnimationController.getShownStateInsets().bottom != 0) { 192 interpolator = SYNC_IME_INTERPOLATOR; 193 duration = ANIMATION_DURATION_SYNC_IME_MS; 194 } else if (triggerBack) { 195 interpolator = FAST_OUT_LINEAR_IN_INTERPOLATOR; 196 duration = ANIMATION_DURATION_UNSYNC_IME_MS; 197 } else { 198 interpolator = EMPHASIZED_DECELERATE; 199 duration = POST_COMMIT_CANCEL_DURATION_MS; 200 } 201 mPostCommitAnimator.setInterpolator(interpolator); 202 mPostCommitAnimator.setDuration(duration); 203 mPostCommitAnimator.addUpdateListener(animation -> { 204 if (mWindowInsetsAnimationController != null) { 205 setInterpolatedProgress((float) animation.getAnimatedValue()); 206 } else { 207 reset(); 208 } 209 }); 210 mPostCommitAnimator.addListener(new AnimatorListenerAdapter() { 211 @Override 212 public void onAnimationEnd(Animator animator) { 213 if (mIsPreCommitAnimationInProgress) { 214 // this means a new gesture has started while the cancel-post-commit-animation 215 // was in progress. Let's not reset anything and let the new user gesture take 216 // over seamlessly 217 return; 218 } 219 if (mWindowInsetsAnimationController != null) { 220 mWindowInsetsAnimationController.finish(!triggerBack); 221 } 222 reset(); 223 } 224 }); 225 mPostCommitAnimator.start(); 226 if (triggerBack) { 227 mInsetsController.setPredictiveBackImeHideAnimInProgress(true); 228 notifyHideIme(); 229 // requesting IME as invisible during post-commit 230 mInsetsController.setRequestedVisibleTypes(0, ime()); 231 mInsetsController.onAnimationStateChanged(ime(), /*running*/ true); 232 } 233 } 234 notifyHideIme()235 private void notifyHideIme() { 236 ImeTracker.Token statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, 237 ImeTracker.ORIGIN_CLIENT, 238 SoftInputShowHideReason.HIDE_SOFT_INPUT_REQUEST_HIDE_WITH_CONTROL, true); 239 // This notifies the IME that it is being hidden. In response, the IME will unregister the 240 // animation callback, such that new back gestures happening during the post-commit phase of 241 // the hide animation can already dispatch to a new callback. 242 // Note that the IME will call hide() in InsetsController. InsetsController will not animate 243 // that hide request if it sees that ImeBackAnimationController is already animating 244 // the IME away 245 mInsetsController.getHost().getInputMethodManager() 246 .notifyImeHidden(mInsetsController.getHost().getWindowToken(), statsToken); 247 } 248 reset()249 private void reset() { 250 mWindowInsetsAnimationController = null; 251 resetPostCommitAnimator(); 252 mLastProgress = 0f; 253 mTriggerBack = false; 254 mIsPreCommitAnimationInProgress = false; 255 mInsetsController.setPredictiveBackImeHideAnimInProgress(false); 256 mStartRootScrollY = 0; 257 } 258 resetPostCommitAnimator()259 private void resetPostCommitAnimator() { 260 if (mPostCommitAnimator != null) { 261 mPostCommitAnimator.cancel(); 262 mPostCommitAnimator = null; 263 } 264 } 265 isBackAnimationAllowed()266 private boolean isBackAnimationAllowed() { 267 268 if (mViewRoot.mContext.getResources().getConfiguration().windowConfiguration 269 .getWindowingMode() == WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW) { 270 // TODO(b/346726115) enable predictive back animation in multi-window mode in 271 // DisplayImeController 272 return false; 273 } 274 275 // otherwise, the predictive back animation is allowed in all cases except when 276 // 1. softInputMode is adjust_resize AND 277 // 2. there is no app-registered WindowInsetsAnimationCallback AND 278 // 3. edge-to-edge is not enabled. 279 return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST) 280 != SOFT_INPUT_ADJUST_RESIZE 281 || (mViewRoot.mView != null && mViewRoot.mView.hasWindowInsetsAnimationCallback()) 282 || mViewRoot.mAttachInfo.mContentOnApplyWindowInsetsListener == null; 283 } 284 isAdjustPan()285 private boolean isAdjustPan() { 286 return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST) 287 == SOFT_INPUT_ADJUST_PAN; 288 } 289 isHideAnimationInProgress()290 private boolean isHideAnimationInProgress() { 291 return mPostCommitAnimator != null && mTriggerBack; 292 } 293 isAnimationInProgress()294 boolean isAnimationInProgress() { 295 return mIsPreCommitAnimationInProgress || mWindowInsetsAnimationController != null; 296 } 297 298 /** 299 * Dump information about this ImeBackAnimationController 300 * 301 * @param prefix the prefix that will be prepended to each line of the produced output 302 * @param writer the writer that will receive the resulting text 303 */ dump(String prefix, PrintWriter writer)304 public void dump(String prefix, PrintWriter writer) { 305 final String innerPrefix = prefix + " "; 306 writer.println(prefix + "ImeBackAnimationController:"); 307 writer.println(innerPrefix + "mLastProgress=" + mLastProgress); 308 writer.println(innerPrefix + "mTriggerBack=" + mTriggerBack); 309 writer.println(innerPrefix + "mIsPreCommitAnimationInProgress=" 310 + mIsPreCommitAnimationInProgress); 311 writer.println(innerPrefix + "mStartRootScrollY=" + mStartRootScrollY); 312 writer.println(innerPrefix + "isBackAnimationAllowed=" + isBackAnimationAllowed()); 313 writer.println(innerPrefix + "isAdjustPan=" + isAdjustPan()); 314 writer.println(innerPrefix + "isHideAnimationInProgress=" 315 + isHideAnimationInProgress()); 316 } 317 318 } 319