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_TYPE_USER; 20 import static android.view.WindowInsets.Type.ime; 21 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; 22 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 23 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ValueAnimator; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.graphics.Insets; 31 import android.util.Log; 32 import android.view.animation.BackGestureInterpolator; 33 import android.view.animation.Interpolator; 34 import android.view.animation.PathInterpolator; 35 import android.view.inputmethod.ImeTracker; 36 import android.window.BackEvent; 37 import android.window.OnBackAnimationCallback; 38 39 import com.android.internal.inputmethod.SoftInputShowHideReason; 40 41 import java.io.PrintWriter; 42 43 /** 44 * Controller for IME predictive back animation 45 * 46 * @hide 47 */ 48 public class ImeBackAnimationController implements OnBackAnimationCallback { 49 50 private static final String TAG = "ImeBackAnimationController"; 51 private static final int POST_COMMIT_DURATION_MS = 200; 52 private static final int POST_COMMIT_CANCEL_DURATION_MS = 50; 53 private static final float PEEK_FRACTION = 0.1f; 54 private static final Interpolator BACK_GESTURE = new BackGestureInterpolator(); 55 private static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( 56 0.05f, 0.7f, 0.1f, 1f); 57 private static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(0.3f, 0f, 1f, 1f); 58 59 private final InsetsController mInsetsController; 60 private final ViewRootImpl mViewRoot; 61 private WindowInsetsAnimationController mWindowInsetsAnimationController = null; 62 private ValueAnimator mPostCommitAnimator = null; 63 private float mLastProgress = 0f; 64 private boolean mTriggerBack = false; 65 private boolean mIsPreCommitAnimationInProgress = false; 66 private int mStartRootScrollY = 0; 67 ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController)68 public ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController) { 69 mInsetsController = insetsController; 70 mViewRoot = viewRoot; 71 } 72 73 @Override onBackStarted(@onNull BackEvent backEvent)74 public void onBackStarted(@NonNull BackEvent backEvent) { 75 if (!isBackAnimationAllowed()) { 76 // There is no good solution for a predictive back animation if the app uses 77 // adjustResize, since we can't relayout the whole app for every frame. We also don't 78 // want to reveal any black areas behind the IME. Therefore let's not play any animation 79 // in that case for now. 80 Log.d(TAG, "onBackStarted -> not playing predictive back animation due to softinput" 81 + " mode adjustResize AND no animation callback registered"); 82 return; 83 } 84 if (isHideAnimationInProgress()) { 85 // If IME is currently animating away, skip back gesture 86 return; 87 } 88 mIsPreCommitAnimationInProgress = true; 89 if (mWindowInsetsAnimationController != null) { 90 // There's still an active animation controller. This means that a cancel post commit 91 // animation of an earlier back gesture is still in progress. Let's cancel it and let 92 // the new gesture seamlessly take over. 93 resetPostCommitAnimator(); 94 setPreCommitProgress(0f); 95 return; 96 } 97 mInsetsController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null, 98 new WindowInsetsAnimationControlListener() { 99 @Override 100 public void onReady(@NonNull WindowInsetsAnimationController controller, 101 @WindowInsets.Type.InsetsType int types) { 102 mWindowInsetsAnimationController = controller; 103 if (isAdjustPan()) mStartRootScrollY = mViewRoot.mScrollY; 104 if (mIsPreCommitAnimationInProgress) { 105 setPreCommitProgress(mLastProgress); 106 } else { 107 // gesture has already finished before IME became ready to animate 108 startPostCommitAnim(mTriggerBack); 109 } 110 } 111 112 @Override 113 public void onFinished(@NonNull WindowInsetsAnimationController controller) { 114 reset(); 115 } 116 117 @Override 118 public void onCancelled(@Nullable WindowInsetsAnimationController controller) { 119 reset(); 120 } 121 }, /*fromIme*/ false, /*durationMs*/ -1, /*interpolator*/ null, ANIMATION_TYPE_USER, 122 /*fromPredictiveBack*/ true); 123 } 124 125 @Override onBackProgressed(@onNull BackEvent backEvent)126 public void onBackProgressed(@NonNull BackEvent backEvent) { 127 mLastProgress = backEvent.getProgress(); 128 setPreCommitProgress(mLastProgress); 129 } 130 131 @Override onBackCancelled()132 public void onBackCancelled() { 133 if (!isBackAnimationAllowed()) return; 134 startPostCommitAnim(/*hideIme*/ false); 135 } 136 137 @Override onBackInvoked()138 public void onBackInvoked() { 139 if (!isBackAnimationAllowed() || !mIsPreCommitAnimationInProgress) { 140 // play regular hide animation if back-animation is not allowed or if insets control has 141 // been cancelled by the system (this can happen in split screen for example) 142 mInsetsController.hide(ime()); 143 return; 144 } 145 startPostCommitAnim(/*hideIme*/ true); 146 } 147 setPreCommitProgress(float progress)148 private void setPreCommitProgress(float progress) { 149 if (isHideAnimationInProgress()) return; 150 if (mWindowInsetsAnimationController != null) { 151 float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; 152 float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom; 153 float imeHeight = shownY - hiddenY; 154 float interpolatedProgress = BACK_GESTURE.getInterpolation(progress); 155 int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION)); 156 if (mStartRootScrollY != 0) { 157 mViewRoot.setScrollY( 158 (int) (mStartRootScrollY * (1 - interpolatedProgress * PEEK_FRACTION))); 159 } 160 mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f, 161 progress); 162 } 163 } 164 startPostCommitAnim(boolean triggerBack)165 private void startPostCommitAnim(boolean triggerBack) { 166 mIsPreCommitAnimationInProgress = false; 167 if (mWindowInsetsAnimationController == null || isHideAnimationInProgress()) { 168 mTriggerBack = triggerBack; 169 return; 170 } 171 mTriggerBack = triggerBack; 172 int currentBottomInset = mWindowInsetsAnimationController.getCurrentInsets().bottom; 173 int targetBottomInset; 174 if (triggerBack) { 175 targetBottomInset = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; 176 } else { 177 targetBottomInset = mWindowInsetsAnimationController.getShownStateInsets().bottom; 178 } 179 mPostCommitAnimator = ValueAnimator.ofFloat(currentBottomInset, targetBottomInset); 180 mPostCommitAnimator.setInterpolator( 181 triggerBack ? STANDARD_ACCELERATE : EMPHASIZED_DECELERATE); 182 mPostCommitAnimator.addUpdateListener(animation -> { 183 int bottomInset = (int) ((float) animation.getAnimatedValue()); 184 if (mWindowInsetsAnimationController != null) { 185 mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, bottomInset), 186 1f, animation.getAnimatedFraction()); 187 } else { 188 reset(); 189 } 190 }); 191 mPostCommitAnimator.addListener(new AnimatorListenerAdapter() { 192 @Override 193 public void onAnimationEnd(Animator animator) { 194 if (mIsPreCommitAnimationInProgress) { 195 // this means a new gesture has started while the cancel-post-commit-animation 196 // was in progress. Let's not reset anything and let the new user gesture take 197 // over seamlessly 198 return; 199 } 200 if (mWindowInsetsAnimationController != null) { 201 mWindowInsetsAnimationController.finish(!triggerBack); 202 } 203 reset(); 204 } 205 }); 206 mPostCommitAnimator.setDuration( 207 triggerBack ? POST_COMMIT_DURATION_MS : POST_COMMIT_CANCEL_DURATION_MS); 208 mPostCommitAnimator.start(); 209 if (triggerBack) { 210 mInsetsController.setPredictiveBackImeHideAnimInProgress(true); 211 notifyHideIme(); 212 } 213 if (mStartRootScrollY != 0 && !triggerBack) { 214 // This causes RootView to update its scroll back to the panned position 215 mInsetsController.getHost().notifyInsetsChanged(); 216 } 217 } 218 notifyHideIme()219 private void notifyHideIme() { 220 ImeTracker.Token statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, 221 ImeTracker.ORIGIN_CLIENT, 222 SoftInputShowHideReason.HIDE_SOFT_INPUT_REQUEST_HIDE_WITH_CONTROL, true); 223 // This notifies the IME that it is being hidden. In response, the IME will unregister the 224 // animation callback, such that new back gestures happening during the post-commit phase of 225 // the hide animation can already dispatch to a new callback. 226 // Note that the IME will call hide() in InsetsController. InsetsController will not animate 227 // that hide request if it sees that ImeBackAnimationController is already animating 228 // the IME away 229 mInsetsController.getHost().getInputMethodManager() 230 .notifyImeHidden(mInsetsController.getHost().getWindowToken(), statsToken); 231 232 // requesting IME as invisible during post-commit 233 mInsetsController.setRequestedVisibleTypes(0, ime()); 234 // Changes the animation state. This also notifies RootView of changed insets, which causes 235 // it to reset its scrollY to 0f (animated) if it was panned 236 mInsetsController.onAnimationStateChanged(ime(), /*running*/ true); 237 } 238 reset()239 private void reset() { 240 mWindowInsetsAnimationController = null; 241 resetPostCommitAnimator(); 242 mLastProgress = 0f; 243 mTriggerBack = false; 244 mIsPreCommitAnimationInProgress = false; 245 mInsetsController.setPredictiveBackImeHideAnimInProgress(false); 246 mStartRootScrollY = 0; 247 } 248 resetPostCommitAnimator()249 private void resetPostCommitAnimator() { 250 if (mPostCommitAnimator != null) { 251 mPostCommitAnimator.cancel(); 252 mPostCommitAnimator = null; 253 } 254 } 255 isBackAnimationAllowed()256 private boolean isBackAnimationAllowed() { 257 // back animation is allowed in all cases except when softInputMode is adjust_resize AND 258 // there is no app-registered WindowInsetsAnimationCallback AND edge-to-edge is not enabled. 259 return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST) 260 != SOFT_INPUT_ADJUST_RESIZE 261 || (mViewRoot.mView != null && mViewRoot.mView.hasWindowInsetsAnimationCallback()) 262 || mViewRoot.mAttachInfo.mContentOnApplyWindowInsetsListener == null; 263 } 264 isAdjustPan()265 private boolean isAdjustPan() { 266 return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST) 267 == SOFT_INPUT_ADJUST_PAN; 268 } 269 isHideAnimationInProgress()270 private boolean isHideAnimationInProgress() { 271 return mPostCommitAnimator != null && mTriggerBack; 272 } 273 274 /** 275 * Dump information about this ImeBackAnimationController 276 * 277 * @param prefix the prefix that will be prepended to each line of the produced output 278 * @param writer the writer that will receive the resulting text 279 */ dump(String prefix, PrintWriter writer)280 public void dump(String prefix, PrintWriter writer) { 281 final String innerPrefix = prefix + " "; 282 writer.println(prefix + "ImeBackAnimationController:"); 283 writer.println(innerPrefix + "mLastProgress=" + mLastProgress); 284 writer.println(innerPrefix + "mTriggerBack=" + mTriggerBack); 285 writer.println(innerPrefix + "mIsPreCommitAnimationInProgress=" 286 + mIsPreCommitAnimationInProgress); 287 writer.println(innerPrefix + "mStartRootScrollY=" + mStartRootScrollY); 288 writer.println(innerPrefix + "isBackAnimationAllowed=" + isBackAnimationAllowed()); 289 writer.println(innerPrefix + "isAdjustPan=" + isAdjustPan()); 290 writer.println(innerPrefix + "isHideAnimationInProgress=" 291 + isHideAnimationInProgress()); 292 } 293 294 } 295