1818import android .util .SparseArray ;
1919import android .view .View ;
2020import android .view .ViewStructure ;
21+ import android .view .WindowInsets ;
22+ import android .view .WindowInsetsAnimation ;
2123import android .view .autofill .AutofillId ;
2224import android .view .autofill .AutofillManager ;
2325import android .view .autofill .AutofillValue ;
2628import android .view .inputmethod .InputConnection ;
2729import android .view .inputmethod .InputMethodManager ;
2830import android .view .inputmethod .InputMethodSubtype ;
29- import android .view .WindowInsets ;
30- import android .view .WindowInsetsAnimation ;
3131import androidx .annotation .NonNull ;
3232import androidx .annotation .Nullable ;
3333import androidx .annotation .RequiresApi ;
@@ -60,6 +60,7 @@ public class TextInputPlugin {
6060 // details.
6161 private boolean isInputConnectionLocked ;
6262
63+ @ SuppressLint ("NewApi" )
6364 public TextInputPlugin (
6465 View view ,
6566 @ NonNull TextInputChannel textInputChannel ,
@@ -72,6 +73,9 @@ public TextInputPlugin(
7273 afm = null ;
7374 }
7475
76+ // Sets up syncing ime insets with the framework, allowing
77+ // the Flutter view to grow and shrink to accomodate Android
78+ // controlled keyboard animations.
7579 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
7680 int mask = 0 ;
7781 if ((View .SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView .getWindowSystemUiVisibility ()) == 0 ) {
@@ -86,8 +90,8 @@ public TextInputPlugin(
8690 mask , // Overlay
8791 WindowInsets .Type .ime () // Deferred
8892 );
89- view .setWindowInsetsAnimationCallback (imeSyncCallback );
90- view .setOnApplyWindowInsetsListener (imeSyncCallback );
93+ mView .setWindowInsetsAnimationCallback (imeSyncCallback );
94+ mView .setOnApplyWindowInsetsListener (imeSyncCallback );
9195 }
9296
9397 this .textInputChannel = textInputChannel ;
@@ -161,12 +165,32 @@ public void sendAppPrivateCommand(String action, Bundle data) {
161165
162166 // Loosely based off of
163167 // https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt
164- // When the IME is shown or hidden, it sends an onApplyWindowInsets call with the
165- // final state of the IME. This defers the final call to allow the animation to
166- // take place before re-calling onApplyWindowInsets after animation completion.
168+ //
169+ // When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call
170+ // with the final state of the IME. This initial call disrupts the animation, which
171+ // causes a flicker in the beginning.
172+ //
173+ // To fix this, this class extends WindowInsetsAnimation.Callback and implements
174+ // OnApplyWindowInsetsListener. We capture and defer the initial call to
175+ // onApplyWindowInsets while the animation completes. When the animation
176+ // finishes, we can then release the call by invoking it in the onEnd callback
177+ //
178+ // The WindowInsetsAnimation.Callback extension forwards the new state of the
179+ // IME inset from onProgress() to the framework. We also make use of the
180+ // onPrepare and onStart callbacks to detect which calls to onApplyWindowInsets
181+ // would interrupt the animation and defer it.
182+ //
183+ // By implementing OnApplyWindowInsetsListener, we are able to capture Android's
184+ // attempts to call the FlutterView's onApplyWindowInsets. When a call to onPrepare
185+ // and subsequently onStart occurs, we can mark any non-animation calls to
186+ // onApplyWindowInsets() that ocurrs between prepare and start as deferred by
187+ // using this class' custom implementation to cache the WindowInsets passed in.
188+ // When onEnd indicates the end of the animation, the deferred call is dispatched
189+ // again, this time avoiding any flicker since the animation is now complete.
167190 @ VisibleForTesting
168191 @ TargetApi (30 )
169192 @ RequiresApi (30 )
193+ @ SuppressLint ({"NewApi" , "Override" })
170194 class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation .Callback
171195 implements View .OnApplyWindowInsetsListener {
172196 private int overlayInsetTypes ;
@@ -192,8 +216,13 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
192216 lastWindowInsets = windowInsets ;
193217 }
194218 if (deferredInsets ) {
219+ // While animation is running, we consume the insets to prevent disrupting
220+ // the animation, which skips this implementation and calls the view's
221+ // onApplyWindowInsets directly to avoid being consumed here.
195222 return WindowInsets .CONSUMED ;
196223 }
224+ // If no animation is happening, pass the insets on to the view's own
225+ // inset handling.
197226 return view .onApplyWindowInsets (windowInsets );
198227 }
199228
@@ -214,7 +243,8 @@ public WindowInsetsAnimation.Bounds onStart(
214243 }
215244
216245 @ Override
217- public WindowInsets onProgress (WindowInsets insets , List <WindowInsetsAnimation > runningAnimations ) {
246+ public WindowInsets onProgress (
247+ WindowInsets insets , List <WindowInsetsAnimation > runningAnimations ) {
218248 if (!deferredInsets ) {
219249 return insets ;
220250 }
@@ -230,6 +260,12 @@ public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation>
230260 }
231261 WindowInsets .Builder builder = new WindowInsets .Builder (lastWindowInsets );
232262 // Overlay the ime-only insets with the full insets.
263+ //
264+ // The IME insets passed in by onProgress assumes that the entire animation
265+ // occurs above any present navigation and status bars. This causes the
266+ // IME inset to be too large for the animation. To remedy this, we merge the
267+ // IME inset with other insets present via a subtract, which causes the IME
268+ // inset to be overlaid with any bars present.
233269 Insets newImeInsets =
234270 Insets .of (
235271 0 ,
@@ -240,7 +276,10 @@ public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation>
240276 - insets .getInsets (overlayInsetTypes ).bottom ,
241277 0 ));
242278 builder .setInsets (deferredInsetTypes , newImeInsets );
243- // Directly call onApplyWindowInsets as we want to skip this class' version of this call.
279+ // Directly call onApplyWindowInsets of the view as we do not want to pass through
280+ // the onApplyWindowInsets defined in this class, which would consume the insets
281+ // as if they were a non-animation inset change and cache it for re-dispatch in
282+ // onEnd instead.
244283 view .onApplyWindowInsets (builder .build ());
245284 return insets ;
246285 }
@@ -315,6 +354,10 @@ public void unlockPlatformViewInputConnection() {
315354 public void destroy () {
316355 platformViewsController .detachTextInputPlugin ();
317356 textInputChannel .setTextInputMethodHandler (null );
357+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
358+ mView .setWindowInsetsAnimationCallback (null );
359+ mView .setOnApplyWindowInsetsListener (null );
360+ }
318361 }
319362
320363 private static int inputTypeFromTextInputType (
0 commit comments