Skip to content

Commit eb423bd

Browse files
committed
# This is a combination of 5 commits.
# This is the 1st commit message: Formatting # This is the commit message flutter#2: Suppress lint # This is the commit message flutter#3: Docs, linter # This is the commit message flutter#4: More docs # This is the commit message #5: Fix whitespace:
1 parent 0d8a5e6 commit eb423bd

File tree

2 files changed

+60
-10
lines changed

2 files changed

+60
-10
lines changed

shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import android.util.SparseArray;
1919
import android.view.View;
2020
import android.view.ViewStructure;
21+
import android.view.WindowInsets;
22+
import android.view.WindowInsetsAnimation;
2123
import android.view.autofill.AutofillId;
2224
import android.view.autofill.AutofillManager;
2325
import android.view.autofill.AutofillValue;
@@ -26,8 +28,6 @@
2628
import android.view.inputmethod.InputConnection;
2729
import android.view.inputmethod.InputMethodManager;
2830
import android.view.inputmethod.InputMethodSubtype;
29-
import android.view.WindowInsets;
30-
import android.view.WindowInsetsAnimation;
3131
import androidx.annotation.NonNull;
3232
import androidx.annotation.Nullable;
3333
import 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(

shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@
3434
import android.view.inputmethod.InputMethodManager;
3535
import android.view.inputmethod.InputMethodSubtype;
3636
import io.flutter.embedding.android.FlutterView;
37+
import io.flutter.embedding.engine.FlutterEngine;
3738
import io.flutter.embedding.engine.FlutterJNI;
3839
import io.flutter.embedding.engine.dart.DartExecutor;
40+
import io.flutter.embedding.engine.loader.FlutterLoader;
41+
import io.flutter.embedding.engine.renderer.FlutterRenderer;
3942
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
4043
import io.flutter.plugin.common.BinaryMessenger;
4144
import io.flutter.plugin.common.JSONMethodCodec;
@@ -50,6 +53,7 @@
5053
import org.junit.Test;
5154
import org.junit.runner.RunWith;
5255
import org.mockito.ArgumentCaptor;
56+
import org.mockito.Mock;
5357
import org.robolectric.RobolectricTestRunner;
5458
import org.robolectric.RuntimeEnvironment;
5559
import org.robolectric.annotation.Config;
@@ -62,6 +66,9 @@
6266
@Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class)
6367
@RunWith(RobolectricTestRunner.class)
6468
public class TextInputPluginTest {
69+
@Mock FlutterJNI mockFlutterJni;
70+
@Mock FlutterLoader mockFlutterLoader;
71+
6572
// Verifies the method and arguments for a captured method call.
6673
private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs)
6774
throws JSONException {
@@ -655,7 +662,7 @@ public void ime_windowInsetsSync() {
655662
spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
656663
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
657664
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);
658-
flutterView.attachToFlutterEngine(flutterEngine);
665+
testView.attachToFlutterEngine(flutterEngine);
659666

660667
WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class);
661668
when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime());

0 commit comments

Comments
 (0)