From 09f226016d19cafc2a91020e92af6fb99db4fc1c Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Tue, 28 Mar 2023 20:09:56 +0800 Subject: [PATCH 1/7] [Android] Send unfocus message to framework to ensure focus state is correct. --- .../systemchannels/TextInputChannel.java | 7 +++++ .../ImeSyncDeferringInsetsCallback.java | 31 +++++++++++++++++++ .../plugin/editing/TextInputPlugin.java | 15 +++++++++ 3 files changed, 53 insertions(+) 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 e0f95681f3b9c..57af2e21ad4ce 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -367,6 +367,13 @@ public void performPrivateCommand( "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); } + /** Instructs Flutter to execute a "unfocus" action. */ + public void unfocus(int inputClientId) { + Log.v(TAG, "Sending 'unfocus' message."); + channel.invokeMethod( + "TextInputClient.unfocus", Arrays.asList(inputClientId, "TextInputAction.unfocus")); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed * from the underlying platform channel. diff --git a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java index 0a6f0ef3f64db..6fc625e27f9ef 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java +++ b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java @@ -14,6 +14,8 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.List; // Loosely based off of @@ -54,6 +56,7 @@ class ImeSyncDeferringInsetsCallback { private WindowInsets lastWindowInsets; private AnimationCallback animationCallback; private InsetsListener insetsListener; + private ImeVisibleListener imeVisibleListener; // True when an animation that matches deferredInsetTypes is active. // @@ -88,6 +91,11 @@ void remove() { view.setOnApplyWindowInsetsListener(null); } + // Set a listener to be notified when the IME visibility changes. + void setImeVisibleListener(ImeVisibleListener imeVisibleListener) { + this.imeVisibleListener = imeVisibleListener; + } + @VisibleForTesting View.OnApplyWindowInsetsListener getInsetsListener() { return insetsListener; @@ -98,6 +106,11 @@ WindowInsetsAnimation.Callback getAnimationCallback() { return animationCallback; } + @VisibleForTesting + ImeVisibleListener getImeVisibleListener() { + return imeVisibleListener; + } + // WindowInsetsAnimation.Callback was introduced in API level 30. The callback // subclass is separated into an inner class in order to avoid warnings from // the Android class loader on older platforms. @@ -115,6 +128,19 @@ public void onPrepare(WindowInsetsAnimation animation) { } } + @NonNull + @Override + public WindowInsetsAnimation.Bounds onStart( + @NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { + // Observe changes to software keyboard visibility and notify listener when animation start. + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view); + if (insets != null && imeVisibleListener != null) { + boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + imeVisibleListener.onImeVisibleChanged(imeVisible); + } + return super.onStart(animation, bounds); + } + @Override public WindowInsets onProgress( WindowInsets insets, List runningAnimations) { @@ -199,4 +225,9 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { return view.onApplyWindowInsets(windowInsets); } } + + // Listener for IME visibility changes. + public interface ImeVisibleListener { + void onImeVisibleChanged(boolean visible); + } } diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 10cbc1b65cb61..065636d493a2d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -94,6 +94,17 @@ public TextInputPlugin( WindowInsets.Type.ime() // Deferred, insets that will animate ); imeSyncCallback.install(); + + // When the keyboard is hidden, unfocus the text field. + imeSyncCallback.setImeVisibleListener( + new ImeSyncDeferringInsetsCallback.ImeVisibleListener() { + @Override + public void onImeVisibleChanged(boolean visible) { + if (!visible) { + unfocus(); + } + } + }); } this.textInputChannel = textInputChannel; @@ -838,4 +849,8 @@ public void autofill(@NonNull SparseArray values) { textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues); } // -------- End: Autofill ------- + + public void unfocus() { + textInputChannel.unfocus(inputTarget.id); + } } From 3dec297b8a57a2ce94aef77366700bb3fd3a43a6 Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 10:29:52 +0800 Subject: [PATCH 2/7] [Android] Send onConnectionClosed message to framework to ensure focus state is correct. --- .../embedding/engine/systemchannels/TextInputChannel.java | 7 ++++--- .../plugin/editing/ImeSyncDeferringInsetsCallback.java | 1 + .../io/flutter/plugin/editing/TextInputPlugin.java | 8 ++++---- 3 files changed, 9 insertions(+), 7 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 57af2e21ad4ce..d88d6b372b271 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -368,10 +368,11 @@ public void performPrivateCommand( } /** Instructs Flutter to execute a "unfocus" action. */ - public void unfocus(int inputClientId) { - Log.v(TAG, "Sending 'unfocus' message."); + public void onConnectionClosed(int inputClientId) { + Log.v(TAG, "Sending 'onConnectionClosed' message."); channel.invokeMethod( - "TextInputClient.unfocus", Arrays.asList(inputClientId, "TextInputAction.unfocus")); + "TextInputClient.onConnectionClosed", + Arrays.asList(inputClientId, "TextInputClient.onConnectionClosed")); } /** diff --git a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java index 6fc625e27f9ef..e33aed5d41552 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java +++ b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java @@ -133,6 +133,7 @@ public void onPrepare(WindowInsetsAnimation animation) { public WindowInsetsAnimation.Bounds onStart( @NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { // Observe changes to software keyboard visibility and notify listener when animation start. + // See https://developer.android.com/develop/ui/views/layout/sw-keyboard. WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view); if (insets != null && imeVisibleListener != null) { boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 065636d493a2d..042d3d2070c4f 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -95,13 +95,13 @@ public TextInputPlugin( ); imeSyncCallback.install(); - // When the keyboard is hidden, unfocus the text field. + // When the IME is hidden, we need to notify the framework that close connection. imeSyncCallback.setImeVisibleListener( new ImeSyncDeferringInsetsCallback.ImeVisibleListener() { @Override public void onImeVisibleChanged(boolean visible) { if (!visible) { - unfocus(); + onConnectionClosed(); } } }); @@ -850,7 +850,7 @@ public void autofill(@NonNull SparseArray values) { } // -------- End: Autofill ------- - public void unfocus() { - textInputChannel.unfocus(inputTarget.id); + public void onConnectionClosed() { + textInputChannel.onConnectionClosed(inputTarget.id); } } From 86cc4a2074bf94f15f434559abeaeb4d43ae9a69 Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 12:31:25 +0800 Subject: [PATCH 3/7] add test. --- .../flutter/plugin/editing/TextInputPluginTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 462b6ec2ff808..f9298a2794c86 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2118,6 +2118,19 @@ public void ime_windowInsetsSync() { assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); } + @Test + @TargetApi(30) + @Config(sdk = 30) + public void onConnectionClosed_imeInvisible() { + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + imeSyncCallback.getImeVisibleListener().onImeVisibleChanged(false); + verify(textInputChannel, times(0)).onConnectionClosed(anyInt()); + } + interface EventHandler { void sendAppPrivateCommand(View view, String action, Bundle data); } From 61f6e6cc7c2358696591025cd2a5d72cd02ab39a Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 12:32:17 +0800 Subject: [PATCH 4/7] add test. --- .../test/io/flutter/plugin/editing/TextInputPluginTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f9298a2794c86..9d60d12c729a4 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2125,7 +2125,7 @@ public void onConnectionClosed_imeInvisible() { FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); imeSyncCallback.getImeVisibleListener().onImeVisibleChanged(false); verify(textInputChannel, times(0)).onConnectionClosed(anyInt()); From b09321217c0451f3f86f1037fe541eb1a9e4e59c Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 13:03:32 +0800 Subject: [PATCH 5/7] add test. --- .../test/io/flutter/plugin/editing/TextInputPluginTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 9d60d12c729a4..1bd4bf4ff9fae 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2122,8 +2122,8 @@ public void ime_windowInsetsSync() { @TargetApi(30) @Config(sdk = 30) public void onConnectionClosed_imeInvisible() { - FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + View testView = new View(ctx); + TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); From e5b1867b37d3a15b7e8f725df06f65c6b30a62dd Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 13:10:21 +0800 Subject: [PATCH 6/7] fix test problem. --- .../test/io/flutter/plugin/editing/TextInputPluginTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1bd4bf4ff9fae..9459a5701f657 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2128,7 +2128,7 @@ public void onConnectionClosed_imeInvisible() { new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); imeSyncCallback.getImeVisibleListener().onImeVisibleChanged(false); - verify(textInputChannel, times(0)).onConnectionClosed(anyInt()); + verify(textInputChannel, times(1)).onConnectionClosed(anyInt()); } interface EventHandler { From 6570eaab5c4792e59b6a53ba080361920175f418 Mon Sep 17 00:00:00 2001 From: ksballetba <2501226111@qq.com> Date: Wed, 29 Mar 2023 13:13:58 +0800 Subject: [PATCH 7/7] fix nits. --- .../embedding/engine/systemchannels/TextInputChannel.java | 2 +- 1 file changed, 1 insertion(+), 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 d88d6b372b271..687d1f29a2a30 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -367,7 +367,7 @@ public void performPrivateCommand( "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); } - /** Instructs Flutter to execute a "unfocus" action. */ + /** Instructs Flutter to execute a "onConnectionClosed" action. */ public void onConnectionClosed(int inputClientId) { Log.v(TAG, "Sending 'onConnectionClosed' message."); channel.invokeMethod(