• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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