diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index c11d264c21988..62abf762a4d68 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -199,7 +199,17 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback private View view; private WindowInsets lastWindowInsets; - private boolean started = false; + // True when an animation that matches deferredInsetTypes is active. + // + // While this is active, this class will capture the initial window inset + // sent into lastWindowInsets by flagging needsSave to true, and will hold + // onto the intitial inset until the animation is completed, when it will + // re-dispatch the inset change. + private boolean animating = false; + // When an animation begins, android sends a WindowInset with the final + // state of the animation. When needsSave is true, we know to capture this + // initial WindowInset. + private boolean needsSave = false; ImeSyncDeferringInsetsCallback( @NonNull View view, int overlayInsetTypes, int deferredInsetTypes) { @@ -212,34 +222,38 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { this.view = view; - if (started) { + if (needsSave) { + // Store the view and insets for us in onEnd() below. This captured inset + // is not part of the animation and instead, represents the final state + // of the inset after the animation is completed. Thus, we defer the processing + // of this WindowInset until the animation completes. + lastWindowInsets = windowInsets; + needsSave = false; + } + if (animating) { // While animation is running, we consume the insets to prevent disrupting // the animation, which skips this implementation and calls the view's // onApplyWindowInsets directly to avoid being consumed here. return WindowInsets.CONSUMED; } - // Store the view and insets for us in onEnd() below - lastWindowInsets = windowInsets; - // If no animation is happening, pass the insets on to the view's own // inset handling. return view.onApplyWindowInsets(windowInsets); } @Override - public WindowInsetsAnimation.Bounds onStart( - WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { + public void onPrepare(WindowInsetsAnimation animation) { if ((animation.getTypeMask() & deferredInsetTypes) != 0) { - started = true; + animating = true; + needsSave = true; } - return bounds; } @Override public WindowInsets onProgress( WindowInsets insets, List runningAnimations) { - if (!started) { + if (!animating || needsSave) { return insets; } boolean matching = false; @@ -280,10 +294,10 @@ public WindowInsets onProgress( @Override public void onEnd(WindowInsetsAnimation animation) { - if (started && (animation.getTypeMask() & deferredInsetTypes) != 0) { + if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) { // If we deferred the IME insets and an IME animation has finished, we need to reset // the flags - started = false; + animating = false; // And finally dispatch the deferred insets to the view now. // Ideally we would just call view.requestApplyInsets() and let the normal dispatch diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 562f1f51dcf13..b038eb56000f3 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -669,6 +669,8 @@ public void ime_windowInsetsSync() { WindowInsets.Builder builder = new WindowInsets.Builder(); WindowInsets noneInsets = builder.build(); + // imeInsets0, 1, and 2 contain unique IME bottom insets, and are used + // to distinguish which insets were sent at each stage. builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100)); builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40)); WindowInsets imeInsets0 = builder.build(); @@ -677,6 +679,10 @@ public void ime_windowInsetsSync() { builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40)); WindowInsets imeInsets1 = builder.build(); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40)); + WindowInsets imeInsets2 = builder.build(); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 200)); builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 0)); WindowInsets deferredInsets = builder.build(); @@ -696,6 +702,8 @@ public void ime_windowInsetsSync() { imeSyncCallback.onPrepare(animation); imeSyncCallback.onApplyWindowInsets(testView, deferredInsets); imeSyncCallback.onStart(animation, null); + // Only the final state call is saved, extra calls are passed on. + imeSyncCallback.onApplyWindowInsets(testView, imeInsets2); verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); // No change, as deferredInset is stored to be passed in onEnd() @@ -723,7 +731,7 @@ public void ime_windowInsetsSync() { imeSyncCallback.onEnd(animation); verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - // Values should be of deferredInsets + // Values should be of deferredInsets, not imeInsets2 assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); assertEquals(10, viewportMetricsCaptor.getValue().paddingTop); assertEquals(200, viewportMetricsCaptor.getValue().viewInsetBottom);