Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

File tree

4 files changed

+71
-1
lines changed

4 files changed

+71
-1
lines changed

shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,14 @@ public void performPrivateCommand(
367367
"TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json));
368368
}
369369

370+
/** Instructs Flutter to execute a "onConnectionClosed" action. */
371+
public void onConnectionClosed(int inputClientId) {
372+
Log.v(TAG, "Sending 'onConnectionClosed' message.");
373+
channel.invokeMethod(
374+
"TextInputClient.onConnectionClosed",
375+
Arrays.asList(inputClientId, "TextInputClient.onConnectionClosed"));
376+
}
377+
370378
/**
371379
* Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed
372380
* from the underlying platform channel.

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import androidx.annotation.NonNull;
1515
import androidx.annotation.RequiresApi;
1616
import androidx.annotation.VisibleForTesting;
17+
import androidx.core.view.ViewCompat;
18+
import androidx.core.view.WindowInsetsCompat;
1719
import java.util.List;
1820

1921
// Loosely based off of
@@ -41,7 +43,9 @@
4143
// a no-op. When onEnd indicates the end of the animation, the deferred call is
4244
// dispatched again, this time avoiding any flicker since the animation is now
4345
// complete.
44-
@VisibleForTesting
46+
47+
// This class should have "package private" visibility cause it's called from TextInputPlugin.
48+
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
4549
@TargetApi(30)
4650
@RequiresApi(30)
4751
@SuppressLint({"NewApi", "Override"})
@@ -54,6 +58,7 @@ class ImeSyncDeferringInsetsCallback {
5458
private WindowInsets lastWindowInsets;
5559
private AnimationCallback animationCallback;
5660
private InsetsListener insetsListener;
61+
private ImeVisibleListener imeVisibleListener;
5762

5863
// True when an animation that matches deferredInsetTypes is active.
5964
//
@@ -88,6 +93,11 @@ void remove() {
8893
view.setOnApplyWindowInsetsListener(null);
8994
}
9095

96+
// Set a listener to be notified when the IME visibility changes.
97+
void setImeVisibleListener(ImeVisibleListener imeVisibleListener) {
98+
this.imeVisibleListener = imeVisibleListener;
99+
}
100+
91101
@VisibleForTesting
92102
View.OnApplyWindowInsetsListener getInsetsListener() {
93103
return insetsListener;
@@ -98,6 +108,11 @@ WindowInsetsAnimation.Callback getAnimationCallback() {
98108
return animationCallback;
99109
}
100110

111+
@VisibleForTesting
112+
ImeVisibleListener getImeVisibleListener() {
113+
return imeVisibleListener;
114+
}
115+
101116
// WindowInsetsAnimation.Callback was introduced in API level 30. The callback
102117
// subclass is separated into an inner class in order to avoid warnings from
103118
// the Android class loader on older platforms.
@@ -115,6 +130,20 @@ public void onPrepare(WindowInsetsAnimation animation) {
115130
}
116131
}
117132

133+
@NonNull
134+
@Override
135+
public WindowInsetsAnimation.Bounds onStart(
136+
@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) {
137+
// Observe changes to software keyboard visibility and notify listener when animation start.
138+
// See https://developer.android.com/develop/ui/views/layout/sw-keyboard.
139+
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
140+
if (insets != null && imeVisibleListener != null) {
141+
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
142+
imeVisibleListener.onImeVisibleChanged(imeVisible);
143+
}
144+
return super.onStart(animation, bounds);
145+
}
146+
118147
@Override
119148
public WindowInsets onProgress(
120149
WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
@@ -199,4 +228,9 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
199228
return view.onApplyWindowInsets(windowInsets);
200229
}
201230
}
231+
232+
// Listener for IME visibility changes.
233+
public interface ImeVisibleListener {
234+
void onImeVisibleChanged(boolean visible);
235+
}
202236
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ public TextInputPlugin(
9494
WindowInsets.Type.ime() // Deferred, insets that will animate
9595
);
9696
imeSyncCallback.install();
97+
98+
// When the IME is hidden, we need to notify the framework that close connection.
99+
imeSyncCallback.setImeVisibleListener(
100+
new ImeSyncDeferringInsetsCallback.ImeVisibleListener() {
101+
@Override
102+
public void onImeVisibleChanged(boolean visible) {
103+
if (!visible) {
104+
onConnectionClosed();
105+
}
106+
}
107+
});
97108
}
98109

99110
this.textInputChannel = textInputChannel;
@@ -838,4 +849,8 @@ public void autofill(@NonNull SparseArray<AutofillValue> values) {
838849
textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues);
839850
}
840851
// -------- End: Autofill -------
852+
853+
public void onConnectionClosed() {
854+
textInputChannel.onConnectionClosed(inputTarget.id);
855+
}
841856
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,6 +2118,19 @@ public void ime_windowInsetsSync() {
21182118
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop);
21192119
}
21202120

2121+
@Test
2122+
@TargetApi(30)
2123+
@Config(sdk = 30)
2124+
public void onConnectionClosed_imeInvisible() {
2125+
View testView = new View(ctx);
2126+
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
2127+
TextInputPlugin textInputPlugin =
2128+
new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
2129+
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
2130+
imeSyncCallback.getImeVisibleListener().onImeVisibleChanged(false);
2131+
verify(textInputChannel, times(1)).onConnectionClosed(anyInt());
2132+
}
2133+
21212134
interface EventHandler {
21222135
void sendAppPrivateCommand(View view, String action, Bundle data);
21232136
}

0 commit comments

Comments
 (0)