From dfaa942e940e594780ac586ea8b60a4fdc8d4203 Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 20 Aug 2020 21:25:32 -0700 Subject: [PATCH 01/13] Initial code --- .../systemchannels/TextInputChannel.java | 7 +++ .../plugin/editing/TextInputPlugin.java | 52 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index b05921c84bb1b..c6cb750118509 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -134,6 +134,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result textInputMethodHandler.finishAutofillContext((boolean) args); result.success(null); break; + + case "TextInput.setWindowInsetsAnimation": + textInputMethodHandler.setWindowInsetsAnimation(); + result.success(null); + break; default: result.notImplemented(); break; @@ -389,6 +394,8 @@ public interface TextInputMethodHandler { * @param data Any data to include with the command. */ void sendAppPrivateCommand(String action, Bundle data); + + void setWindowInsetsAnimation(); } /** A text editing configuration. */ diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index a96b0deeabb22..63afb93f9eb20 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -4,6 +4,7 @@ package io.flutter.plugin.editing; +import android.util.Log; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Rect; @@ -24,6 +25,10 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimationController; +import android.view.WindowInsetsAnimationControlListener; +import android.view.animation.LinearInterpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -70,12 +75,14 @@ public TextInputPlugin( new TextInputChannel.TextInputMethodHandler() { @Override public void show() { + controlTextInputWindowInsetsAnimation(); showTextInput(mView); } @Override public void hide() { - hideTextInput(mView); + controlTextInputWindowInsetsAnimation(); + // hideTextInput(mView); } @Override @@ -125,6 +132,16 @@ public void clearClient() { public void sendAppPrivateCommand(String action, Bundle data) { sendTextInputAppPrivateCommand(action, data); } + + @Override + public void setWindowInsetsAnimation( + // long durationMillis, + // Interpolator interpolator, + // CancellationSignal cancellationSignal, + // WindowInsetsAnimationControlListener listener + ) { + controlTextInputWindowInsetsAnimation(); + } }); textInputChannel.requestExistingInputState(); @@ -134,6 +151,39 @@ public void sendAppPrivateCommand(String action, Bundle data) { restartAlwaysRequired = isRestartAlwaysRequired(); } + private class CustomWindowInsetsAnimationControlListener implements WindowInsetsAnimationControlListener { + CustomWindowInsetsAnimationControlListener() {} + + public void onCancelled(WindowInsetsAnimationController controller) { + + } + + public void onFinished(WindowInsetsAnimationController controller) { + + } + + public void onReady(WindowInsetsAnimationController controller, int types) { + + } + } + private void controlTextInputWindowInsetsAnimation( + // int types, + // long durationMillis, + // Interpolator interpolator, + // CancellationSignal cancellationSignal, + // WindowInsetsAnimationControlListener listener + ) { + Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!0"); + mView.getWindowInsetsController().controlWindowInsetsAnimation( + // WindowInsets.Type.IME, + 4, + -1,// duration. + new LinearInterpolator(), + null, + new CustomWindowInsetsAnimationControlListener() + ); + } + @NonNull public InputMethodManager getInputMethodManager() { return mImm; From af4d7a85c897d602dfc262429de58777e002f253 Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 27 Aug 2020 04:04:10 -0700 Subject: [PATCH 02/13] Additional core impl --- .../systemchannels/TextInputChannel.java | 15 +++-- .../plugin/editing/TextInputPlugin.java | 63 ++++++++++--------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index c6cb750118509..9428b0f51ae9b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -135,9 +135,16 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(null); break; - case "TextInput.setWindowInsetsAnimation": - textInputMethodHandler.setWindowInsetsAnimation(); - result.success(null); + case "TextInput.setKeyboardInset": + textInputMethodHandler.setKeyboardInset(); + try { + final JSONObject arguments = (JSONObject) args; + final int bottomInset = arguments.getInt("bottomInset"); + textInputMethodHandler.sendAppPrivateCommand(bottomInset); + result.success(null); + } catch (JSONException exception) { + result.error("error", exception.getMessage(), null); + } break; default: result.notImplemented(); @@ -395,7 +402,7 @@ public interface TextInputMethodHandler { */ void sendAppPrivateCommand(String action, Bundle data); - void setWindowInsetsAnimation(); + void setKeyboardInset(int bottomInset); } /** A text editing configuration. */ diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 63afb93f9eb20..dfd8da71d0484 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -7,6 +7,7 @@ import android.util.Log; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Insets; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; @@ -51,6 +52,7 @@ public class TextInputPlugin { @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private final boolean restartAlwaysRequired; + private CustomWindowInsetsAnimationControlListener mInsetsListener; // When true following calls to createInputConnection will return the cached lastInputConnection // if the input @@ -75,14 +77,14 @@ public TextInputPlugin( new TextInputChannel.TextInputMethodHandler() { @Override public void show() { - controlTextInputWindowInsetsAnimation(); + controlTextInputWindowInsetsAnimation(150); showTextInput(mView); } @Override public void hide() { - controlTextInputWindowInsetsAnimation(); - // hideTextInput(mView); + controlTextInputWindowInsetsAnimation(100); + hideTextInput(mView); } @Override @@ -134,13 +136,8 @@ public void sendAppPrivateCommand(String action, Bundle data) { } @Override - public void setWindowInsetsAnimation( - // long durationMillis, - // Interpolator interpolator, - // CancellationSignal cancellationSignal, - // WindowInsetsAnimationControlListener listener - ) { - controlTextInputWindowInsetsAnimation(); + public void setKeyboardInset(int bottomInset) { + controlTextInputWindowInsetsAnimation(bottomInset); } }); @@ -152,35 +149,43 @@ public void setWindowInsetsAnimation( } private class CustomWindowInsetsAnimationControlListener implements WindowInsetsAnimationControlListener { - CustomWindowInsetsAnimationControlListener() {} - - public void onCancelled(WindowInsetsAnimationController controller) { + private Insets targetInsets; + CustomWindowInsetsAnimationControlListener() { + targetInsets = Insets.of(0, 0, 0, 0); } - public void onFinished(WindowInsetsAnimationController controller) { + CustomWindowInsetsAnimationControlListener(Insets insets) { + targetInsets = insets; + } + void setInsets(Insets insets) { + targetInsets = insets; } - public void onReady(WindowInsetsAnimationController controller, int types) { + public void onCancelled(WindowInsetsAnimationController controller) {} + + public void onFinished(WindowInsetsAnimationController controller) {} + public void onReady(WindowInsetsAnimationController controller, int types) { + controller.setInsetsAndAlpha(targetInsets, 1f, 1f); } } - private void controlTextInputWindowInsetsAnimation( - // int types, - // long durationMillis, - // Interpolator interpolator, - // CancellationSignal cancellationSignal, - // WindowInsetsAnimationControlListener listener - ) { - Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!0"); + + private void controlTextInputWindowInsetsAnimation(int bottomInset) { + Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!: " + bottomInset); + Insets targetInsets = Insets.of(0, 0, 0, bottomInset); + if (mInsetsListener == null) { + mInsetsListener = new CustomWindowInsetsAnimationControlListener(targetInsets); + } else { + mInsetsListener.setInsets(targetInsets); + } mView.getWindowInsetsController().controlWindowInsetsAnimation( - // WindowInsets.Type.IME, - 4, - -1,// duration. - new LinearInterpolator(), - null, - new CustomWindowInsetsAnimationControlListener() + android.view.WindowInsets.Type.ime(), + -1, // duration. + null, // interpolator + null, // cancellationSignal + mInsetsListener ); } From 7f03868e3bc5f185039238285d8795a0552bbbd7 Mon Sep 17 00:00:00 2001 From: garyqian Date: Tue, 1 Sep 2020 03:16:38 -0700 Subject: [PATCH 03/13] PArtially working --- .../src/engine/text_editing/text_editing.dart | 13 +++++ .../systemchannels/TextInputChannel.java | 3 +- .../plugin/editing/TextInputPlugin.java | 58 +++++++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index c2dc4e6755763..d6d4d4b32ee60 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -876,6 +876,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _lastEditingState!.applyToDomElement(domElement); } + void setKeyboardInset(int bottomInset) { + print('DART:UI SETTING KEYBOARD INSET IN DEFAULTTEXTEDITINGSTRATEGY'); + } + void placeElement() { domElement.focus(); } @@ -1321,6 +1325,10 @@ class TextEditingChannel { cleanForms(); break; + case 'TextInput.setKeyboardInset': + implementation.setKeyboardInset(call.arguments); + break; + default: throw StateError( 'Unsupported method call on the flutter/textinput channel: ${call.method}'); @@ -1510,6 +1518,11 @@ class HybridTextEditing { } } + /// Responds to the 'TextInput.setKeyboardInset' message. + void setKeyboardInset(int bottomInset) { + editingElement!.setKeyboardInset(bottomInset); + } + /// A CSS class name used to identify all elements used for text editing. @visibleForTesting static const String textEditingClass = 'flt-text-editing'; diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 9428b0f51ae9b..837a2a494ead5 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -136,11 +136,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result break; case "TextInput.setKeyboardInset": - textInputMethodHandler.setKeyboardInset(); try { final JSONObject arguments = (JSONObject) args; final int bottomInset = arguments.getInt("bottomInset"); - textInputMethodHandler.sendAppPrivateCommand(bottomInset); + textInputMethodHandler.setKeyboardInset(bottomInset); result.success(null); } catch (JSONException exception) { result.error("error", exception.getMessage(), null); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index dfd8da71d0484..66be80fe4d5e8 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -77,13 +77,13 @@ public TextInputPlugin( new TextInputChannel.TextInputMethodHandler() { @Override public void show() { - controlTextInputWindowInsetsAnimation(150); + // controlTextInputWindowInsetsAnimation(150); showTextInput(mView); } @Override public void hide() { - controlTextInputWindowInsetsAnimation(100); + // controlTextInputWindowInsetsAnimation(100); hideTextInput(mView); } @@ -149,18 +149,25 @@ public void setKeyboardInset(int bottomInset) { } private class CustomWindowInsetsAnimationControlListener implements WindowInsetsAnimationControlListener { - private Insets targetInsets; + private int offset; + int baseInset; CustomWindowInsetsAnimationControlListener() { - targetInsets = Insets.of(0, 0, 0, 0); + // targetInsets = Insets.of(0, 0, 0, 0); } - CustomWindowInsetsAnimationControlListener(Insets insets) { - targetInsets = insets; + void setOffset(int offset) { + this.offset = offset; } - void setInsets(Insets insets) { - targetInsets = insets; + void setBaseInset(int baseInset) { + this.baseInset = baseInset; + } + + void computeBaseInset(View view) { + int mask = android.view.WindowInsets.Type.ime(); + Insets finalInsets = view.getRootWindowInsets().getInsets(mask); + if (baseInset < finalInsets.bottom) setBaseInset(finalInsets.bottom); } public void onCancelled(WindowInsetsAnimationController controller) {} @@ -168,18 +175,25 @@ public void onCancelled(WindowInsetsAnimationController controller) {} public void onFinished(WindowInsetsAnimationController controller) {} public void onReady(WindowInsetsAnimationController controller, int types) { - controller.setInsetsAndAlpha(targetInsets, 1f, 1f); + if (controller.isReady() && (controller.getTypes() & types) > 0) { + controller.setInsetsAndAlpha(Insets.of(0, 0, 0, baseInset + offset), 1f, 1f); + } + // controller.finish(true); } } - private void controlTextInputWindowInsetsAnimation(int bottomInset) { - Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!: " + bottomInset); - Insets targetInsets = Insets.of(0, 0, 0, bottomInset); - if (mInsetsListener == null) { - mInsetsListener = new CustomWindowInsetsAnimationControlListener(targetInsets); - } else { - mInsetsListener.setInsets(targetInsets); - } + private void controlTextInputWindowInsetsAnimation(int offset) { + Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!: " + offset + " " + mInsetsListener.baseInset); + // if (mInsetsListener == null) { + // mInsetsListener = new CustomWindowInsetsAnimationControlListener(); + // mInsetsListener.setBaseInset(0); + // mInsetsListener.setOffset(offset); + // } else { + // mInsetsListener.setOffset(offset); + // } + setupInsetsListener(); + mInsetsListener.setOffset(offset); + mInsetsListener.computeBaseInset(mView); mView.getWindowInsetsController().controlWindowInsetsAnimation( android.view.WindowInsets.Type.ime(), -1, // duration. @@ -189,6 +203,12 @@ private void controlTextInputWindowInsetsAnimation(int bottomInset) { ); } + private void setupInsetsListener() { + if (mInsetsListener == null) { + mInsetsListener = new CustomWindowInsetsAnimationControlListener(); + } + } + @NonNull public InputMethodManager getInputMethodManager() { return mImm; @@ -371,6 +391,8 @@ public void sendTextInputAppPrivateCommand(String action, Bundle data) { private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); + setupInsetsListener(); + mInsetsListener.computeBaseInset(mView); } private void hideTextInput(View view) { @@ -382,6 +404,8 @@ private void hideTextInput(View view) { // field(by text field here I mean anything that keeps the keyboard open). // See: https://github.com/flutter/flutter/issues/34169 mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); + setupInsetsListener(); + mInsetsListener.computeBaseInset(mView); } private void notifyViewEntered() { From 794304f9607d4e10775e5e26b9eaf69bdcd4d495 Mon Sep 17 00:00:00 2001 From: garyqian Date: Tue, 1 Sep 2020 04:47:54 -0700 Subject: [PATCH 04/13] DispatchOnAttach --- .../flutter/plugin/editing/TextInputPlugin.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 66be80fe4d5e8..8ca9e981b7ea1 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -167,15 +167,22 @@ void setBaseInset(int baseInset) { void computeBaseInset(View view) { int mask = android.view.WindowInsets.Type.ime(); Insets finalInsets = view.getRootWindowInsets().getInsets(mask); + Log.e("flutter", "CURRENT VIEW BOTTOM INSET: " + finalInsets.bottom); if (baseInset < finalInsets.bottom) setBaseInset(finalInsets.bottom); } - public void onCancelled(WindowInsetsAnimationController controller) {} + public void onCancelled(WindowInsetsAnimationController controller) { + Log.e("flutter", " CANCELLED"); + } - public void onFinished(WindowInsetsAnimationController controller) {} + public void onFinished(WindowInsetsAnimationController controller) { + Log.e("flutter", " FINISHED"); + } public void onReady(WindowInsetsAnimationController controller, int types) { + Log.e("flutter", " READY"); if (controller.isReady() && (controller.getTypes() & types) > 0) { + Log.e("flutter", " READY SET " + offset + " " + baseInset); controller.setInsetsAndAlpha(Insets.of(0, 0, 0, baseInset + offset), 1f, 1f); } // controller.finish(true); @@ -201,6 +208,10 @@ private void controlTextInputWindowInsetsAnimation(int offset) { null, // cancellationSignal mInsetsListener ); + WindowInsets.Builder builder = new WindowInsets.Builder(mView.getRootWindowInsets()); + builder.setInsets(android.view.WindowInsets.Type.ime(), Insets.of(0, 0, 0, mInsetsListener.baseInset + offset)); + + mView.dispatchApplyWindowInsets(builder.build()); } private void setupInsetsListener() { From 3642f611ae01c4c0e4eb3d42c67954658deec20b Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 05:20:09 -0700 Subject: [PATCH 05/13] Embedder->framework sync initial --- .../embedding/android/FlutterView.java | 1 + .../plugin/editing/TextInputPlugin.java | 145 +++++++++++++++++- .../android/io/flutter/view/FlutterView.java | 2 + 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 2f1942034a7e5..45322a662dd2b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -537,6 +537,7 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { viewportMetrics.paddingLeft = uiInsets.left; Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime()); + Log.e("flutter", "IME Insets: " + imeInsets); viewportMetrics.viewInsetTop = imeInsets.top; viewportMetrics.viewInsetRight = imeInsets.right; viewportMetrics.viewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 8ca9e981b7ea1..06d9a032e6ecb 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -27,6 +27,7 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.WindowInsetsAnimationController; import android.view.WindowInsetsAnimationControlListener; import android.view.animation.LinearInterpolator; @@ -36,6 +37,7 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.platform.PlatformViewsController; import java.util.HashMap; +import java.util.List; /** Android implementation of the text input plugin. */ public class TextInputPlugin { @@ -72,18 +74,27 @@ public TextInputPlugin( afm = null; } + RootViewDeferringInsetsCallback callback = new RootViewDeferringInsetsCallback( + view, + WindowInsets.Type.systemBars(), // Persistent + WindowInsets.Type.ime() // Deferred + ); + view.setWindowInsetsAnimationCallback(callback); + view.setOnApplyWindowInsetsListener(callback); + + this.textInputChannel = textInputChannel; textInputChannel.setTextInputMethodHandler( new TextInputChannel.TextInputMethodHandler() { @Override public void show() { - // controlTextInputWindowInsetsAnimation(150); + Log.e("flutter", "SHOWING KEYBOARD"); showTextInput(mView); } @Override public void hide() { - // controlTextInputWindowInsetsAnimation(100); + Log.e("flutter", "HIDING KEYBOARD"); hideTextInput(mView); } @@ -137,7 +148,7 @@ public void sendAppPrivateCommand(String action, Bundle data) { @Override public void setKeyboardInset(int bottomInset) { - controlTextInputWindowInsetsAnimation(bottomInset); + // controlTextInputWindowInsetsAnimation(bottomInset); } }); @@ -189,6 +200,129 @@ public void onReady(WindowInsetsAnimationController controller, int types) { } } + private class SyncWindowInsetsAnimationCallback extends WindowInsetsAnimation.Callback { + private View view; + + public SyncWindowInsetsAnimationCallback(View view, int dispatchMode) { + super(dispatchMode); + this.view = view; + } + + public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { + Log.e("flutter", "STARTING"); + return null; + } + + public WindowInsets onProgress(WindowInsets insets, List runningAnimations) { + view.dispatchApplyWindowInsets(insets); + return insets; + } + + public void onEnd(WindowInsetsAnimation animation) { + Log.e("flutter", "ENDING"); + + } + } + + private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { + private int persistentInsetTypes; + private int deferredInsetTypes; + + private View view; + private WindowInsets lastWindowInsets; + private boolean deferredInsets = false; + private boolean started = false; + + private View rootView; + + RootViewDeferringInsetsCallback(View view, int persistentInsetTypes, int deferredInsetTypes) { + super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE); + this.persistentInsetTypes = persistentInsetTypes; + this.deferredInsetTypes = deferredInsetTypes; + this.rootView = view; + } + + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { + // Store the view and insets for us in onEnd() below + this.view = view; + if (!started) { + lastWindowInsets = windowInsets; + Log.e("flutter", "Deferring: " + windowInsets.getInsets(deferredInsetTypes)); + } + + // // When the deferred flag is enabled, we only use the systemBars() insets + // int types = persistentInsetTypes; + // // Otherwise we handle the combination of the the systemBars() and ime() insets + + if (deferredInsets) { + return WindowInsets.CONSUMED; + } + return view.onApplyWindowInsets(windowInsets); + } + + @Override + public void onPrepare(WindowInsetsAnimation animation) { + if ((animation.getTypeMask() & deferredInsetTypes) != 0) { + Log.e("flutter", "PREPING"); + // We defer the WindowInsets.Type.ime() insets if the IME is currently not visible. + // This results in only the WindowInsets.Type.systemBars() being applied, allowing + // the scrolling view to remain at it's larger size. + deferredInsets = true; + } + } + + @Override + public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { + Log.e("flutter", "STARTING"); + if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { + started = true; + } + return bounds; + } + + @Override + public WindowInsets onProgress(WindowInsets insets, List runningAnims) { + if (!deferredInsets) { + return insets; + } + boolean matching = false; + for (WindowInsetsAnimation animation : runningAnims) { + if ((animation.getTypeMask() & deferredInsetTypes) != 0) { + matching = true; + continue; + } + } + if (!matching) { + return insets; + } + // WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); + // builder.setInsets(deferredInsetTypes, insets.getInsets(deferredInsetTypes)); + // rootView.onApplyWindowInsets(builder.build()); + rootView.onApplyWindowInsets(insets); + return insets; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + Log.e("flutter", "ENDING"); + if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { + // If we deferred the IME insets and an IME animation has finished, we need to reset + // the flag + deferredInsets = false; + started = false; + + // And finally dispatch the deferred insets to the view now. + // Ideally we would just call view.requestApplyInsets() and let the normal dispatch + // cycle happen, but this happens too late resulting in a visual flicker. + // Instead we manually dispatch the most recent WindowInsets to the view. + if (lastWindowInsets != null && view != null) { + view.dispatchApplyWindowInsets(lastWindowInsets); + } + } + } + } + private void controlTextInputWindowInsetsAnimation(int offset) { Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!: " + offset + " " + mInsetsListener.baseInset); // if (mInsetsListener == null) { @@ -402,8 +536,7 @@ public void sendTextInputAppPrivateCommand(String action, Bundle data) { private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); - setupInsetsListener(); - mInsetsListener.computeBaseInset(mView); + view.requestApplyInsets(); } private void hideTextInput(View view) { @@ -415,8 +548,6 @@ private void hideTextInput(View view) { // field(by text field here I mean anything that keeps the keyboard open). // See: https://github.com/flutter/flutter/issues/34169 mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); - setupInsetsListener(); - mInsetsListener.computeBaseInset(mView); } private void notifyViewEntered() { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 2ed917b9f918d..7871300f04f4a 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -603,6 +603,8 @@ public final WindowInsets onApplyWindowInsets(WindowInsets insets) { (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Thread.dumpStack(); + int mask = 0; if (navigationBarVisible) { mask = mask | android.view.WindowInsets.Type.navigationBars(); From bc5c37fecc95c4850c3ce35a1823de9657fe994c Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 09:20:25 -0700 Subject: [PATCH 06/13] Working --- .../plugin/editing/TextInputPlugin.java | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 06d9a032e6ecb..aa6312eced901 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -30,6 +30,7 @@ import android.view.WindowInsetsAnimation; import android.view.WindowInsetsAnimationController; import android.view.WindowInsetsAnimationControlListener; +import android.view.WindowInsetsController; import android.view.animation.LinearInterpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -74,9 +75,20 @@ public TextInputPlugin( afm = null; } + // boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0; + // boolean navigationBarVisible = + // (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0; + int mask = 0; + if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) { + mask = mask | WindowInsets.Type.navigationBars(); + } + if ((View.SYSTEM_UI_FLAG_FULLSCREEN & mView.getWindowSystemUiVisibility()) == 0) { + mask = mask | WindowInsets.Type.statusBars(); + } + // mask = mask | WindowInsets.Type.systemGestures(); RootViewDeferringInsetsCallback callback = new RootViewDeferringInsetsCallback( view, - WindowInsets.Type.systemBars(), // Persistent + mask, // Persistent WindowInsets.Type.ime() // Deferred ); view.setWindowInsetsAnimationCallback(callback); @@ -250,11 +262,6 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { lastWindowInsets = windowInsets; Log.e("flutter", "Deferring: " + windowInsets.getInsets(deferredInsetTypes)); } - - // // When the deferred flag is enabled, we only use the systemBars() insets - // int types = persistentInsetTypes; - // // Otherwise we handle the combination of the the systemBars() and ime() insets - if (deferredInsets) { return WindowInsets.CONSUMED; } @@ -282,12 +289,12 @@ public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, Win } @Override - public WindowInsets onProgress(WindowInsets insets, List runningAnims) { + public WindowInsets onProgress(WindowInsets insets, List runningAnimations) { if (!deferredInsets) { return insets; } boolean matching = false; - for (WindowInsetsAnimation animation : runningAnims) { + for (WindowInsetsAnimation animation : runningAnimations) { if ((animation.getTypeMask() & deferredInsetTypes) != 0) { matching = true; continue; @@ -296,10 +303,16 @@ public WindowInsets onProgress(WindowInsets insets, List if (!matching) { return insets; } - // WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); - // builder.setInsets(deferredInsetTypes, insets.getInsets(deferredInsetTypes)); - // rootView.onApplyWindowInsets(builder.build()); - rootView.onApplyWindowInsets(insets); + WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); + Insets persistentInsets = insets.getInsets(persistentInsetTypes); + // Overlay the ime-only insets with the full insets. + Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(persistentInsetTypes).bottom, 0)); + // Insets newImeInsets = Insets.max(lastWindowInsets.getInsets(WindowInsets.Type.navigationBars()), Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom, 0))); + Log.e("flutter", "Progress: " + persistentInsets + " " + persistentInsetTypes); + // builder.setInsets(WindowInsets.Type.navigationBars(), newImeInsets); + builder.setInsets(deferredInsetTypes, newImeInsets); + rootView.onApplyWindowInsets(builder.build()); + // rootView.onApplyWindowInsets(insets); return insets; } @@ -536,7 +549,12 @@ public void sendTextInputAppPrivateCommand(String action, Bundle data) { private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); - view.requestApplyInsets(); + // view.requestApplyInsets(); + + // WindowInsetsController controller = mView.getWindowInsetsController(); + + // Show the keyboard (IME) + // controller.show(WindowInsets.Type.ime()); } private void hideTextInput(View view) { From cc6399c28e567590ef435e9e948106223d97d92f Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 09:52:05 -0700 Subject: [PATCH 07/13] Cleanup --- .../src/engine/text_editing/text_editing.dart | 13 ------ .../embedding/android/FlutterView.java | 1 - .../plugin/editing/TextInputPlugin.java | 46 ------------------- 3 files changed, 60 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d6d4d4b32ee60..c2dc4e6755763 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -876,10 +876,6 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy { _lastEditingState!.applyToDomElement(domElement); } - void setKeyboardInset(int bottomInset) { - print('DART:UI SETTING KEYBOARD INSET IN DEFAULTTEXTEDITINGSTRATEGY'); - } - void placeElement() { domElement.focus(); } @@ -1325,10 +1321,6 @@ class TextEditingChannel { cleanForms(); break; - case 'TextInput.setKeyboardInset': - implementation.setKeyboardInset(call.arguments); - break; - default: throw StateError( 'Unsupported method call on the flutter/textinput channel: ${call.method}'); @@ -1518,11 +1510,6 @@ class HybridTextEditing { } } - /// Responds to the 'TextInput.setKeyboardInset' message. - void setKeyboardInset(int bottomInset) { - editingElement!.setKeyboardInset(bottomInset); - } - /// A CSS class name used to identify all elements used for text editing. @visibleForTesting static const String textEditingClass = 'flt-text-editing'; diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 45322a662dd2b..2f1942034a7e5 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -537,7 +537,6 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { viewportMetrics.paddingLeft = uiInsets.left; Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime()); - Log.e("flutter", "IME Insets: " + imeInsets); viewportMetrics.viewInsetTop = imeInsets.top; viewportMetrics.viewInsetRight = imeInsets.right; viewportMetrics.viewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index aa6312eced901..f0bce5431bb9c 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -212,30 +212,6 @@ public void onReady(WindowInsetsAnimationController controller, int types) { } } - private class SyncWindowInsetsAnimationCallback extends WindowInsetsAnimation.Callback { - private View view; - - public SyncWindowInsetsAnimationCallback(View view, int dispatchMode) { - super(dispatchMode); - this.view = view; - } - - public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { - Log.e("flutter", "STARTING"); - return null; - } - - public WindowInsets onProgress(WindowInsets insets, List runningAnimations) { - view.dispatchApplyWindowInsets(insets); - return insets; - } - - public void onEnd(WindowInsetsAnimation animation) { - Log.e("flutter", "ENDING"); - - } - } - private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { private int persistentInsetTypes; private int deferredInsetTypes; @@ -260,7 +236,6 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { this.view = view; if (!started) { lastWindowInsets = windowInsets; - Log.e("flutter", "Deferring: " + windowInsets.getInsets(deferredInsetTypes)); } if (deferredInsets) { return WindowInsets.CONSUMED; @@ -271,7 +246,6 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { @Override public void onPrepare(WindowInsetsAnimation animation) { if ((animation.getTypeMask() & deferredInsetTypes) != 0) { - Log.e("flutter", "PREPING"); // We defer the WindowInsets.Type.ime() insets if the IME is currently not visible. // This results in only the WindowInsets.Type.systemBars() being applied, allowing // the scrolling view to remain at it's larger size. @@ -281,7 +255,6 @@ public void onPrepare(WindowInsetsAnimation animation) { @Override public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { - Log.e("flutter", "STARTING"); if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { started = true; } @@ -307,18 +280,13 @@ public WindowInsets onProgress(WindowInsets insets, List Insets persistentInsets = insets.getInsets(persistentInsetTypes); // Overlay the ime-only insets with the full insets. Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(persistentInsetTypes).bottom, 0)); - // Insets newImeInsets = Insets.max(lastWindowInsets.getInsets(WindowInsets.Type.navigationBars()), Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom, 0))); - Log.e("flutter", "Progress: " + persistentInsets + " " + persistentInsetTypes); - // builder.setInsets(WindowInsets.Type.navigationBars(), newImeInsets); builder.setInsets(deferredInsetTypes, newImeInsets); rootView.onApplyWindowInsets(builder.build()); - // rootView.onApplyWindowInsets(insets); return insets; } @Override public void onEnd(WindowInsetsAnimation animation) { - Log.e("flutter", "ENDING"); if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { // If we deferred the IME insets and an IME animation has finished, we need to reset // the flag @@ -337,14 +305,6 @@ public void onEnd(WindowInsetsAnimation animation) { } private void controlTextInputWindowInsetsAnimation(int offset) { - Log.e("flutter", "IN THE WINDOW WindowInsetsAnimationController !!!!!!!!!: " + offset + " " + mInsetsListener.baseInset); - // if (mInsetsListener == null) { - // mInsetsListener = new CustomWindowInsetsAnimationControlListener(); - // mInsetsListener.setBaseInset(0); - // mInsetsListener.setOffset(offset); - // } else { - // mInsetsListener.setOffset(offset); - // } setupInsetsListener(); mInsetsListener.setOffset(offset); mInsetsListener.computeBaseInset(mView); @@ -549,12 +509,6 @@ public void sendTextInputAppPrivateCommand(String action, Bundle data) { private void showTextInput(View view) { view.requestFocus(); mImm.showSoftInput(view, 0); - // view.requestApplyInsets(); - - // WindowInsetsController controller = mView.getWindowInsetsController(); - - // Show the keyboard (IME) - // controller.show(WindowInsets.Type.ime()); } private void hideTextInput(View view) { From 45dbba483ad656ecd1d3a6b0ea0bc50882f0f46a Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 10:02:30 -0700 Subject: [PATCH 08/13] Remove framework->embedder --- .../systemchannels/TextInputChannel.java | 13 ---- .../plugin/editing/TextInputPlugin.java | 72 ------------------- 2 files changed, 85 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 837a2a494ead5..b05921c84bb1b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -134,17 +134,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result textInputMethodHandler.finishAutofillContext((boolean) args); result.success(null); break; - - case "TextInput.setKeyboardInset": - try { - final JSONObject arguments = (JSONObject) args; - final int bottomInset = arguments.getInt("bottomInset"); - textInputMethodHandler.setKeyboardInset(bottomInset); - result.success(null); - } catch (JSONException exception) { - result.error("error", exception.getMessage(), null); - } - break; default: result.notImplemented(); break; @@ -400,8 +389,6 @@ public interface TextInputMethodHandler { * @param data Any data to include with the command. */ void sendAppPrivateCommand(String action, Bundle data); - - void setKeyboardInset(int bottomInset); } /** A text editing configuration. */ diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index f0bce5431bb9c..8e78d696a5227 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -55,7 +55,6 @@ public class TextInputPlugin { @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private final boolean restartAlwaysRequired; - private CustomWindowInsetsAnimationControlListener mInsetsListener; // When true following calls to createInputConnection will return the cached lastInputConnection // if the input @@ -100,13 +99,11 @@ public TextInputPlugin( new TextInputChannel.TextInputMethodHandler() { @Override public void show() { - Log.e("flutter", "SHOWING KEYBOARD"); showTextInput(mView); } @Override public void hide() { - Log.e("flutter", "HIDING KEYBOARD"); hideTextInput(mView); } @@ -157,11 +154,6 @@ public void clearClient() { public void sendAppPrivateCommand(String action, Bundle data) { sendTextInputAppPrivateCommand(action, data); } - - @Override - public void setKeyboardInset(int bottomInset) { - // controlTextInputWindowInsetsAnimation(bottomInset); - } }); textInputChannel.requestExistingInputState(); @@ -171,47 +163,6 @@ public void setKeyboardInset(int bottomInset) { restartAlwaysRequired = isRestartAlwaysRequired(); } - private class CustomWindowInsetsAnimationControlListener implements WindowInsetsAnimationControlListener { - private int offset; - int baseInset; - - CustomWindowInsetsAnimationControlListener() { - // targetInsets = Insets.of(0, 0, 0, 0); - } - - void setOffset(int offset) { - this.offset = offset; - } - - void setBaseInset(int baseInset) { - this.baseInset = baseInset; - } - - void computeBaseInset(View view) { - int mask = android.view.WindowInsets.Type.ime(); - Insets finalInsets = view.getRootWindowInsets().getInsets(mask); - Log.e("flutter", "CURRENT VIEW BOTTOM INSET: " + finalInsets.bottom); - if (baseInset < finalInsets.bottom) setBaseInset(finalInsets.bottom); - } - - public void onCancelled(WindowInsetsAnimationController controller) { - Log.e("flutter", " CANCELLED"); - } - - public void onFinished(WindowInsetsAnimationController controller) { - Log.e("flutter", " FINISHED"); - } - - public void onReady(WindowInsetsAnimationController controller, int types) { - Log.e("flutter", " READY"); - if (controller.isReady() && (controller.getTypes() & types) > 0) { - Log.e("flutter", " READY SET " + offset + " " + baseInset); - controller.setInsetsAndAlpha(Insets.of(0, 0, 0, baseInset + offset), 1f, 1f); - } - // controller.finish(true); - } - } - private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { private int persistentInsetTypes; private int deferredInsetTypes; @@ -304,29 +255,6 @@ public void onEnd(WindowInsetsAnimation animation) { } } - private void controlTextInputWindowInsetsAnimation(int offset) { - setupInsetsListener(); - mInsetsListener.setOffset(offset); - mInsetsListener.computeBaseInset(mView); - mView.getWindowInsetsController().controlWindowInsetsAnimation( - android.view.WindowInsets.Type.ime(), - -1, // duration. - null, // interpolator - null, // cancellationSignal - mInsetsListener - ); - WindowInsets.Builder builder = new WindowInsets.Builder(mView.getRootWindowInsets()); - builder.setInsets(android.view.WindowInsets.Type.ime(), Insets.of(0, 0, 0, mInsetsListener.baseInset + offset)); - - mView.dispatchApplyWindowInsets(builder.build()); - } - - private void setupInsetsListener() { - if (mInsetsListener == null) { - mInsetsListener = new CustomWindowInsetsAnimationControlListener(); - } - } - @NonNull public InputMethodManager getInputMethodManager() { return mImm; From 693fe22674cf5adfed862392e3feb42dfa28f541 Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 10:19:24 -0700 Subject: [PATCH 09/13] More cleanup --- .../plugin/editing/TextInputPlugin.java | 23 ++++--------------- .../android/io/flutter/view/FlutterView.java | 2 -- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 8e78d696a5227..e9351120c5dff 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -4,7 +4,6 @@ package io.flutter.plugin.editing; -import android.util.Log; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Insets; @@ -28,10 +27,6 @@ import android.view.inputmethod.InputMethodSubtype; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; -import android.view.WindowInsetsAnimationController; -import android.view.WindowInsetsAnimationControlListener; -import android.view.WindowInsetsController; -import android.view.animation.LinearInterpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -74,9 +69,6 @@ public TextInputPlugin( afm = null; } - // boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0; - // boolean navigationBarVisible = - // (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0; int mask = 0; if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) { mask = mask | WindowInsets.Type.navigationBars(); @@ -84,7 +76,6 @@ public TextInputPlugin( if ((View.SYSTEM_UI_FLAG_FULLSCREEN & mView.getWindowSystemUiVisibility()) == 0) { mask = mask | WindowInsets.Type.statusBars(); } - // mask = mask | WindowInsets.Type.systemGestures(); RootViewDeferringInsetsCallback callback = new RootViewDeferringInsetsCallback( view, mask, // Persistent @@ -172,20 +163,18 @@ private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Call private boolean deferredInsets = false; private boolean started = false; - private View rootView; - RootViewDeferringInsetsCallback(View view, int persistentInsetTypes, int deferredInsetTypes) { super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE); this.persistentInsetTypes = persistentInsetTypes; this.deferredInsetTypes = deferredInsetTypes; - this.rootView = view; + this.view = view; } @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { - // Store the view and insets for us in onEnd() below this.view = view; if (!started) { + // Store the view and insets for us in onEnd() below lastWindowInsets = windowInsets; } if (deferredInsets) { @@ -197,9 +186,6 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { @Override public void onPrepare(WindowInsetsAnimation animation) { if ((animation.getTypeMask() & deferredInsetTypes) != 0) { - // We defer the WindowInsets.Type.ime() insets if the IME is currently not visible. - // This results in only the WindowInsets.Type.systemBars() being applied, allowing - // the scrolling view to remain at it's larger size. deferredInsets = true; } } @@ -232,7 +218,8 @@ public WindowInsets onProgress(WindowInsets insets, List // Overlay the ime-only insets with the full insets. Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(persistentInsetTypes).bottom, 0)); builder.setInsets(deferredInsetTypes, newImeInsets); - rootView.onApplyWindowInsets(builder.build()); + // Directly call onApplyWindowInsets as we want to skip this class' version of this call. + view.onApplyWindowInsets(builder.build()); return insets; } @@ -240,7 +227,7 @@ public WindowInsets onProgress(WindowInsets insets, List public void onEnd(WindowInsetsAnimation animation) { if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { // If we deferred the IME insets and an IME animation has finished, we need to reset - // the flag + // the flags deferredInsets = false; started = false; diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 7871300f04f4a..2ed917b9f918d 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -603,8 +603,6 @@ public final WindowInsets onApplyWindowInsets(WindowInsets insets) { (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Thread.dumpStack(); - int mask = 0; if (navigationBarVisible) { mask = mask | android.view.WindowInsets.Type.navigationBars(); From 158f3e417fc57e1c71a57780f9a42e331e90b97d Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 14:11:36 -0700 Subject: [PATCH 10/13] Version gate, add test --- .../plugin/editing/TextInputPlugin.java | 54 +++++++---- .../plugin/editing/TextInputPluginTest.java | 95 +++++++++++++++++++ 2 files changed, 130 insertions(+), 19 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index e9351120c5dff..0d619aa26dfac 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -5,6 +5,7 @@ package io.flutter.plugin.editing; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Insets; import android.graphics.Rect; @@ -29,6 +30,7 @@ import android.view.WindowInsetsAnimation; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.platform.PlatformViewsController; @@ -50,6 +52,7 @@ public class TextInputPlugin { @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private final boolean restartAlwaysRequired; + private ImeSyncDeferringInsetsCallback imeSyncCallback; // When true following calls to createInputConnection will return the cached lastInputConnection // if the input @@ -69,20 +72,22 @@ public TextInputPlugin( afm = null; } - int mask = 0; - if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) { - mask = mask | WindowInsets.Type.navigationBars(); - } - if ((View.SYSTEM_UI_FLAG_FULLSCREEN & mView.getWindowSystemUiVisibility()) == 0) { - mask = mask | WindowInsets.Type.statusBars(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + int mask = 0; + if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) { + mask = mask | WindowInsets.Type.navigationBars(); + } + if ((View.SYSTEM_UI_FLAG_FULLSCREEN & mView.getWindowSystemUiVisibility()) == 0) { + mask = mask | WindowInsets.Type.statusBars(); + } + imeSyncCallback = new ImeSyncDeferringInsetsCallback( + view, + mask, // Overlay + WindowInsets.Type.ime() // Deferred + ); + view.setWindowInsetsAnimationCallback(imeSyncCallback); + view.setOnApplyWindowInsetsListener(imeSyncCallback); } - RootViewDeferringInsetsCallback callback = new RootViewDeferringInsetsCallback( - view, - mask, // Persistent - WindowInsets.Type.ime() // Deferred - ); - view.setWindowInsetsAnimationCallback(callback); - view.setOnApplyWindowInsetsListener(callback); this.textInputChannel = textInputChannel; @@ -154,8 +159,15 @@ public void sendAppPrivateCommand(String action, Bundle data) { restartAlwaysRequired = isRestartAlwaysRequired(); } - private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { - private int persistentInsetTypes; + // Loosely based off of https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt + // When the IME is shown or hidden, it sends an onApplyWindowInsets call with the + // final state of the IME. This defers the final call to allow the animation to + // take place before re-calling onApplyWindowInsets after animation completion. + @VisibleForTesting + @TargetApi(30) + @RequiresApi(30) + class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { + private int overlayInsetTypes; private int deferredInsetTypes; private View view; @@ -163,9 +175,9 @@ private class RootViewDeferringInsetsCallback extends WindowInsetsAnimation.Call private boolean deferredInsets = false; private boolean started = false; - RootViewDeferringInsetsCallback(View view, int persistentInsetTypes, int deferredInsetTypes) { + ImeSyncDeferringInsetsCallback(View view, int persistentInsetTypes, int deferredInsetTypes) { super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE); - this.persistentInsetTypes = persistentInsetTypes; + this.overlayInsetTypes = overlayInsetTypes; this.deferredInsetTypes = deferredInsetTypes; this.view = view; } @@ -214,9 +226,8 @@ public WindowInsets onProgress(WindowInsets insets, List return insets; } WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); - Insets persistentInsets = insets.getInsets(persistentInsetTypes); // Overlay the ime-only insets with the full insets. - Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(persistentInsetTypes).bottom, 0)); + Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(overlayInsetTypes).bottom, 0)); builder.setInsets(deferredInsetTypes, newImeInsets); // Directly call onApplyWindowInsets as we want to skip this class' version of this call. view.onApplyWindowInsets(builder.build()); @@ -252,6 +263,11 @@ Editable getEditable() { return mEditable; } + @VisibleForTesting + ImeSyncDeferringInsetsCallback getImeSyncCallback() { + return imeSyncCallback; + } + /** * Use the current platform view input connection until unlockPlatformViewInputConnection is * called. 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 1a6b53c6b7080..0d8949207163a 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -15,8 +15,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.AssetManager; +import android.graphics.Insets; import android.os.Build; import android.os.Bundle; import android.provider.Settings; @@ -29,6 +31,8 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; @@ -39,6 +43,7 @@ import io.flutter.plugin.platform.PlatformViewsController; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -632,6 +637,96 @@ public void sendAppPrivateCommand_hasData() throws JSONException { assertEquals("actionData", bundleCaptor.getValue().getCharSequence("data")); } + @Test + @TargetApi(30) + @Config(sdk = 30) + public void ime_windowinsetssync() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + + FlutterView testView = new FlutterView(RuntimeEnvironment.application); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + TextInputPlugin.ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + flutterView.attachToFlutterEngine(flutterEngine); + + WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); + when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); + + List animationList = new ArrayList(); + animationList.add(animation); + + WindowInsets.Builder builder = new WindowInsets.Builder(); + WindowInsets noneInsets = builder.build(); + + 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(); + + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 30)); + 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, 200)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 0)); + WindowInsets deferredInsets = builder.build(); + + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + + imeSyncCallback.onApplyWindowInsets(testView, deferredInsets); + imeSyncCallback.onApplyWindowInsets(testView, noneInsets); + + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(0, viewportMetricsCaptor.getValue().paddingTop); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); + + imeSyncCallback.onPrepare(animation); + imeSyncCallback.onApplyWindowInsets(testView, deferredInsets); + imeSyncCallback.onStart(animation, null); + + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + // No change, as deferredInset is stored to be passed in onEnd() + assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(0, viewportMetricsCaptor.getValue().paddingTop); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); + + imeSyncCallback.onProgress(imeInsets0, animationList); + + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(40, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(10, viewportMetricsCaptor.getValue().paddingTop); + assertEquals(60, viewportMetricsCaptor.getValue().viewInsetBottom); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); + + imeSyncCallback.onProgress(imeInsets1, animationList); + + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(40, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(10, viewportMetricsCaptor.getValue().paddingTop); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); // Cannot be negative + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); + + imeSyncCallback.onEnd(animation); + + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + // Values should be of deferredInsets + assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(10, viewportMetricsCaptor.getValue().paddingTop); + assertEquals(200, viewportMetricsCaptor.getValue().viewInsetBottom); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); + + } + interface EventHandler { void sendAppPrivateCommand(View view, String action, Bundle data); } From 0d8a5e674010d6930b1c2e05055405aaee63989c Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 14:32:56 -0700 Subject: [PATCH 11/13] Formatting --- .../plugin/editing/TextInputPlugin.java | 33 ++++++++++++------- .../plugin/editing/TextInputPluginTest.java | 10 +++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 0d619aa26dfac..cc8d095070e6c 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -80,16 +80,16 @@ public TextInputPlugin( if ((View.SYSTEM_UI_FLAG_FULLSCREEN & mView.getWindowSystemUiVisibility()) == 0) { mask = mask | WindowInsets.Type.statusBars(); } - imeSyncCallback = new ImeSyncDeferringInsetsCallback( - view, - mask, // Overlay - WindowInsets.Type.ime() // Deferred - ); + imeSyncCallback = + new ImeSyncDeferringInsetsCallback( + view, + mask, // Overlay + WindowInsets.Type.ime() // Deferred + ); view.setWindowInsetsAnimationCallback(imeSyncCallback); view.setOnApplyWindowInsetsListener(imeSyncCallback); } - this.textInputChannel = textInputChannel; textInputChannel.setTextInputMethodHandler( new TextInputChannel.TextInputMethodHandler() { @@ -159,14 +159,16 @@ public void sendAppPrivateCommand(String action, Bundle data) { restartAlwaysRequired = isRestartAlwaysRequired(); } - // Loosely based off of https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt + // Loosely based off of + // https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt // When the IME is shown or hidden, it sends an onApplyWindowInsets call with the // final state of the IME. This defers the final call to allow the animation to // take place before re-calling onApplyWindowInsets after animation completion. @VisibleForTesting @TargetApi(30) @RequiresApi(30) - class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { + class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback + implements View.OnApplyWindowInsetsListener { private int overlayInsetTypes; private int deferredInsetTypes; @@ -203,7 +205,8 @@ public void onPrepare(WindowInsetsAnimation animation) { } @Override - public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { + public WindowInsetsAnimation.Bounds onStart( + WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { started = true; } @@ -215,7 +218,7 @@ public WindowInsets onProgress(WindowInsets insets, List if (!deferredInsets) { return insets; } - boolean matching = false; + boolean matching = false; for (WindowInsetsAnimation animation : runningAnimations) { if ((animation.getTypeMask() & deferredInsetTypes) != 0) { matching = true; @@ -227,7 +230,15 @@ public WindowInsets onProgress(WindowInsets insets, List } WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); // Overlay the ime-only insets with the full insets. - Insets newImeInsets = Insets.of(0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - insets.getInsets(overlayInsetTypes).bottom, 0)); + Insets newImeInsets = + Insets.of( + 0, + 0, + 0, + Math.max( + insets.getInsets(deferredInsetTypes).bottom + - insets.getInsets(overlayInsetTypes).bottom, + 0)); builder.setInsets(deferredInsetTypes, newImeInsets); // Directly call onApplyWindowInsets as we want to skip this class' version of this call. view.onApplyWindowInsets(builder.build()); 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 0d8949207163a..8cb35259c5090 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -26,13 +26,13 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewStructure; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import android.view.WindowInsets; -import android.view.WindowInsetsAnimation; import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; @@ -640,7 +640,7 @@ public void sendAppPrivateCommand_hasData() throws JSONException { @Test @TargetApi(30) @Config(sdk = 30) - public void ime_windowinsetssync() { + public void ime_windowInsetsSync() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return; } @@ -649,7 +649,8 @@ public void ime_windowinsetssync() { TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); - TextInputPlugin.ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + TextInputPlugin.ImeSyncDeferringInsetsCallback imeSyncCallback = + textInputPlugin.getImeSyncCallback(); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -724,7 +725,6 @@ public void ime_windowinsetssync() { assertEquals(10, viewportMetricsCaptor.getValue().paddingTop); assertEquals(200, viewportMetricsCaptor.getValue().viewInsetBottom); assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); - } interface EventHandler { From e7b3adce817fe63ebb9e89d4053853ed7e73b6f7 Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 14:44:50 -0700 Subject: [PATCH 12/13] Docs, bugfixes, linter --- .../plugin/editing/TextInputPlugin.java | 95 +++++++++++++------ .../plugin/editing/TextInputPluginTest.java | 9 +- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index cc8d095070e6c..c11d264c21988 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -18,6 +18,8 @@ import android.util.SparseArray; import android.view.View; import android.view.ViewStructure; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; @@ -26,8 +28,6 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import android.view.WindowInsets; -import android.view.WindowInsetsAnimation; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -60,6 +60,7 @@ public class TextInputPlugin { // details. private boolean isInputConnectionLocked; + @SuppressLint("NewApi") public TextInputPlugin( View view, @NonNull TextInputChannel textInputChannel, @@ -72,6 +73,9 @@ public TextInputPlugin( afm = null; } + // Sets up syncing ime insets with the framework, allowing + // the Flutter view to grow and shrink to accomodate Android + // controlled keyboard animations. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { int mask = 0; if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) { @@ -83,11 +87,11 @@ public TextInputPlugin( imeSyncCallback = new ImeSyncDeferringInsetsCallback( view, - mask, // Overlay - WindowInsets.Type.ime() // Deferred + mask, // Overlay, insets that should be merged with the deferred insets + WindowInsets.Type.ime() // Deferred, insets that will animate ); - view.setWindowInsetsAnimationCallback(imeSyncCallback); - view.setOnApplyWindowInsetsListener(imeSyncCallback); + mView.setWindowInsetsAnimationCallback(imeSyncCallback); + mView.setOnApplyWindowInsetsListener(imeSyncCallback); } this.textInputChannel = textInputChannel; @@ -161,12 +165,33 @@ public void sendAppPrivateCommand(String action, Bundle data) { // Loosely based off of // https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt - // When the IME is shown or hidden, it sends an onApplyWindowInsets call with the - // final state of the IME. This defers the final call to allow the animation to - // take place before re-calling onApplyWindowInsets after animation completion. + // + // When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call + // with the final state of the IME. This initial call disrupts the animation, which + // causes a flicker in the beginning. + // + // To fix this, this class extends WindowInsetsAnimation.Callback and implements + // OnApplyWindowInsetsListener. We capture and defer the initial call to + // onApplyWindowInsets while the animation completes. When the animation + // finishes, we can then release the call by invoking it in the onEnd callback + // + // The WindowInsetsAnimation.Callback extension forwards the new state of the + // IME inset from onProgress() to the framework. We also make use of the + // onStart callback to detect which calls to onApplyWindowInsets would + // interrupt the animation and defer it. + // + // By implementing OnApplyWindowInsetsListener, we are able to capture Android's + // attempts to call the FlutterView's onApplyWindowInsets. When a call to onStart + // occurs, we can mark any non-animation calls to onApplyWindowInsets() that + // occurs between prepare and start as deferred by using this class' wrapper + // implementation to cache the WindowInsets passed in and turn the current call into + // a no-op. When onEnd indicates the end of the animation, the deferred call is + // dispatched again, this time avoiding any flicker since the animation is now + // complete. @VisibleForTesting @TargetApi(30) @RequiresApi(30) + @SuppressLint({"NewApi", "Override"}) class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback implements View.OnApplyWindowInsetsListener { private int overlayInsetTypes; @@ -174,10 +199,10 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback private View view; private WindowInsets lastWindowInsets; - private boolean deferredInsets = false; private boolean started = false; - ImeSyncDeferringInsetsCallback(View view, int persistentInsetTypes, int deferredInsetTypes) { + ImeSyncDeferringInsetsCallback( + @NonNull View view, int overlayInsetTypes, int deferredInsetTypes) { super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE); this.overlayInsetTypes = overlayInsetTypes; this.deferredInsetTypes = deferredInsetTypes; @@ -187,35 +212,34 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { this.view = view; - if (!started) { - // Store the view and insets for us in onEnd() below - lastWindowInsets = windowInsets; - } - if (deferredInsets) { + if (started) { + // 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; } - return view.onApplyWindowInsets(windowInsets); - } - @Override - public void onPrepare(WindowInsetsAnimation animation) { - if ((animation.getTypeMask() & deferredInsetTypes) != 0) { - deferredInsets = true; - } + // 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) { - if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { + if ((animation.getTypeMask() & deferredInsetTypes) != 0) { started = true; } return bounds; } @Override - public WindowInsets onProgress(WindowInsets insets, List runningAnimations) { - if (!deferredInsets) { + public WindowInsets onProgress( + WindowInsets insets, List runningAnimations) { + if (!started) { return insets; } boolean matching = false; @@ -230,6 +254,12 @@ public WindowInsets onProgress(WindowInsets insets, List } WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets); // Overlay the ime-only insets with the full insets. + // + // The IME insets passed in by onProgress assumes that the entire animation + // occurs above any present navigation and status bars. This causes the + // IME inset to be too large for the animation. To remedy this, we merge the + // IME inset with other insets present via a subtract + reLu, which causes the + // IME inset to be overlaid with any bars present. Insets newImeInsets = Insets.of( 0, @@ -240,17 +270,19 @@ public WindowInsets onProgress(WindowInsets insets, List - insets.getInsets(overlayInsetTypes).bottom, 0)); builder.setInsets(deferredInsetTypes, newImeInsets); - // Directly call onApplyWindowInsets as we want to skip this class' version of this call. + // Directly call onApplyWindowInsets of the view as we do not want to pass through + // the onApplyWindowInsets defined in this class, which would consume the insets + // as if they were a non-animation inset change and cache it for re-dispatch in + // onEnd instead. view.onApplyWindowInsets(builder.build()); return insets; } @Override public void onEnd(WindowInsetsAnimation animation) { - if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { + if (started && (animation.getTypeMask() & deferredInsetTypes) != 0) { // If we deferred the IME insets and an IME animation has finished, we need to reset // the flags - deferredInsets = false; started = false; // And finally dispatch the deferred insets to the view now. @@ -312,9 +344,14 @@ public void unlockPlatformViewInputConnection() { * *

The TextInputPlugin instance should not be used after calling this. */ + @SuppressLint("NewApi") public void destroy() { platformViewsController.detachTextInputPlugin(); textInputChannel.setTextInputMethodHandler(null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + mView.setWindowInsetsAnimationCallback(null); + mView.setOnApplyWindowInsetsListener(null); + } } private static int inputTypeFromTextInputType( 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 8cb35259c5090..1f789052eeb05 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -34,8 +34,11 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMethodCodec; @@ -50,6 +53,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -62,6 +66,9 @@ @Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class) @RunWith(RobolectricTestRunner.class) public class TextInputPluginTest { + @Mock FlutterJNI mockFlutterJni; + @Mock FlutterLoader mockFlutterLoader; + // Verifies the method and arguments for a captured method call. private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs) throws JSONException { @@ -655,7 +662,7 @@ public void ime_windowInsetsSync() { spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - flutterView.attachToFlutterEngine(flutterEngine); + testView.attachToFlutterEngine(flutterEngine); WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); From 6b0bfae7c7832b1336942dceb938619335bf450f Mon Sep 17 00:00:00 2001 From: garyqian Date: Thu, 3 Sep 2020 18:55:11 -0700 Subject: [PATCH 13/13] Remove extra version check --- .../test/io/flutter/plugin/editing/TextInputPluginTest.java | 4 ---- 1 file changed, 4 deletions(-) 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 1f789052eeb05..562f1f51dcf13 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -648,10 +648,6 @@ public void sendAppPrivateCommand_hasData() throws JSONException { @TargetApi(30) @Config(sdk = 30) public void ime_windowInsetsSync() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - return; - } - FlutterView testView = new FlutterView(RuntimeEnvironment.application); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin =