Skip to content

Commit e80df36

Browse files
authored
Keyboard support for embedded Android views. (#9203)
Generally what this PR is doing is setting up a delegation mechanism for Android's onCreateInputConnection. It works by letting the framework know when an embedded view gets loses focus(within the virtual display), the framework maintains a focus node for each Android view that is kept in sync with the focus state of the embedded view. The TextInputPlugin is extended to allow for 2 type of text clients a "framework client"(what we had before) and a "platform view client". When the AndroidView's focus node in the framework is focused the framework sets a "platform view text client" for the TextInputPlugin, which will result in the TextInputPlugin delegating createInputConnection to the platform view. When a platform view is resized, we are detaching it from a virtual display and attaching it to a new one, as a side affect a platform view might lose an active input connection, to workaround that we "lock" the connection when resizing(by caching it and forcing the cached copy until the resize is done). Additional things worth calling out in this PR: To properly answer which views are allowed for input connection proxying we compare a candidate view's root view to the set of root views of all virtual displays. We also preserve a view's focus state across resizes. Note that this PR only wires text for the io.flutter.view.FlutterView For the new Android embedding some additional plumbing is necessary. Corresponding framework PR: #33901 #19718
1 parent 2ec6dad commit e80df36

File tree

9 files changed

+294
-29
lines changed

9 files changed

+294
-29
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,8 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
477477
// in a way that Flutter understands.
478478
textInputPlugin = new TextInputPlugin(
479479
this,
480-
this.flutterEngine.getDartExecutor()
480+
this.flutterEngine.getDartExecutor(),
481+
null
481482
);
482483
androidKeyProcessor = new AndroidKeyProcessor(
483484
this.flutterEngine.getKeyEventChannel(),

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public class PlatformViewsChannel {
2626
private final MethodChannel channel;
2727
private PlatformViewsHandler handler;
2828

29+
public void invokeViewFocused(int viewId) {
30+
if (channel == null) {
31+
return;
32+
}
33+
channel.invokeMethod("viewFocused", viewId);
34+
}
35+
2936
private final MethodChannel.MethodCallHandler parsingHandler = new MethodChannel.MethodCallHandler() {
3037
@Override
3138
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
@@ -51,6 +58,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
5158
case "setDirection":
5259
setDirection(call, result);
5360
break;
61+
case "clearFocus":
62+
clearFocus(call, result);
63+
break;
5464
default:
5565
result.notImplemented();
5666
}
@@ -172,6 +182,20 @@ private void setDirection(@NonNull MethodCall call, @NonNull MethodChannel.Resul
172182
);
173183
}
174184
}
185+
186+
private void clearFocus(MethodCall call, MethodChannel.Result result) {
187+
int viewId = call.arguments();
188+
try {
189+
handler.clearFocus(viewId);
190+
result.success(null);
191+
} catch (IllegalStateException exception) {
192+
result.error(
193+
"error",
194+
exception.getMessage(),
195+
null
196+
);
197+
}
198+
}
175199
};
176200

177201
/**
@@ -241,6 +265,11 @@ void resizePlatformView(
241265
*/
242266
// TODO(mattcarroll): Introduce an annotation for @TextureId
243267
void setDirection(int viewId, int direction);
268+
269+
/**
270+
* Clears the focus from the platform view with a give id if it is currently focused.
271+
*/
272+
void clearFocus(int viewId);
244273
}
245274

246275
/**

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
7070
result.error("error", exception.getMessage(), null);
7171
}
7272
break;
73+
case "TextInput.setPlatformViewClient":
74+
final int id = (int) args;
75+
textInputMethodHandler.setPlatformViewClient(id);
76+
break;
7377
case "TextInput.setEditingState":
7478
try {
7579
final JSONObject editingState = (JSONObject) args;
@@ -218,6 +222,16 @@ public interface TextInputMethodHandler {
218222
// TODO(mattcarroll): javadoc
219223
void setClient(int textInputClientId, @NonNull Configuration configuration);
220224

225+
/**
226+
* Sets a platform view as the text input client.
227+
*
228+
* Subsequent calls to createInputConnection will be delegated to the platform view until a
229+
* different client is set.
230+
*
231+
* @param id the ID of the platform view to be set as a text input client.
232+
*/
233+
void setPlatformViewClient(int id);
234+
221235
// TODO(mattcarroll): javadoc
222236
void setEditingState(@NonNull TextEditState editingState);
223237

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

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import io.flutter.embedding.engine.dart.DartExecutor;
2020
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
21-
import io.flutter.view.FlutterView;
21+
import io.flutter.plugin.platform.PlatformViewsController;
2222

2323
/**
2424
* Android implementation of the text input plugin.
@@ -30,7 +30,8 @@ public class TextInputPlugin {
3030
private final InputMethodManager mImm;
3131
@NonNull
3232
private final TextInputChannel textInputChannel;
33-
private int mClient = 0;
33+
@NonNull
34+
private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
3435
@Nullable
3536
private TextInputChannel.Configuration configuration;
3637
@Nullable
@@ -39,7 +40,13 @@ public class TextInputPlugin {
3940
@Nullable
4041
private InputConnection lastInputConnection;
4142

42-
public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor) {
43+
private PlatformViewsController platformViewsController;
44+
45+
// When true following calls to createInputConnection will return the cached lastInputConnection if the input
46+
// target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
47+
private boolean isInputConnectionLocked;
48+
49+
public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, PlatformViewsController platformViewsController) {
4350
mView = view;
4451
mImm = (InputMethodManager) view.getContext().getSystemService(
4552
Context.INPUT_METHOD_SERVICE);
@@ -61,6 +68,11 @@ public void setClient(int textInputClientId, TextInputChannel.Configuration conf
6168
setTextInputClient(textInputClientId, configuration);
6269
}
6370

71+
@Override
72+
public void setPlatformViewClient(int platformViewId) {
73+
setPlatformViewTextInputClient(platformViewId);
74+
}
75+
6476
@Override
6577
public void setEditingState(TextInputChannel.TextEditState editingState) {
6678
setTextInputEditingState(mView, editingState);
@@ -71,13 +83,49 @@ public void clearClient() {
7183
clearTextInputClient();
7284
}
7385
});
86+
this.platformViewsController = platformViewsController;
87+
platformViewsController.attachTextInputPlugin(this);
7488
}
7589

7690
@NonNull
7791
public InputMethodManager getInputMethodManager() {
7892
return mImm;
7993
}
8094

95+
/***
96+
* Use the current platform view input connection until unlockPlatformViewInputConnection is called.
97+
*
98+
* The current input connection instance is cached and any following call to @{link createInputConnection} returns
99+
* the cached connection until unlockPlatformViewInputConnection is called.
100+
*
101+
* This is a no-op if the current input target isn't a platform view.
102+
*
103+
* This is used to preserve an input connection when moving a platform view from one virtual display to another.
104+
*/
105+
public void lockPlatformViewInputConnection() {
106+
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
107+
isInputConnectionLocked = true;
108+
}
109+
}
110+
111+
/**
112+
* Unlocks the input connection.
113+
*
114+
* See also: @{link lockPlatformViewInputConnection}.
115+
*/
116+
public void unlockPlatformViewInputConnection() {
117+
isInputConnectionLocked = false;
118+
}
119+
120+
/**
121+
* Detaches the text input plugin from the platform views controller.
122+
*
123+
* The TextInputPlugin instance should not be used after calling this.
124+
*/
125+
public void destroy() {
126+
platformViewsController.detachTextInputPlugin();
127+
}
128+
81129
private static int inputTypeFromTextInputType(
82130
TextInputChannel.InputType type,
83131
boolean obscureText,
@@ -128,8 +176,16 @@ private static int inputTypeFromTextInputType(
128176
}
129177

130178
public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
131-
if (mClient == 0) {
179+
if (inputTarget.type == InputTarget.Type.NO_TARGET) {
132180
lastInputConnection = null;
181+
return null;
182+
}
183+
184+
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
185+
if (isInputConnectionLocked) {
186+
return lastInputConnection;
187+
}
188+
lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs);
133189
return lastInputConnection;
134190
}
135191

@@ -158,7 +214,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
158214

159215
InputConnectionAdaptor connection = new InputConnectionAdaptor(
160216
view,
161-
mClient,
217+
inputTarget.id,
162218
textInputChannel,
163219
mEditable
164220
);
@@ -180,17 +236,26 @@ private void showTextInput(View view) {
180236
}
181237

182238
private void hideTextInput(View view) {
183-
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
239+
if (inputTarget.type == InputTarget.Type.FRAMEWORK_CLIENT) {
240+
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
241+
}
184242
}
185243

186244
private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
187-
mClient = client;
245+
inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
188246
this.configuration = configuration;
189247
mEditable = Editable.Factory.getInstance().newEditable("");
190248

191249
// setTextInputClient will be followed by a call to setTextInputEditingState.
192250
// Do a restartInput at that time.
193251
mRestartInputPending = true;
252+
unlockPlatformViewInputConnection();
253+
}
254+
255+
private void setPlatformViewTextInputClient(int platformViewId) {
256+
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
257+
mImm.restartInput(mView);
258+
mRestartInputPending = false;
194259
}
195260

196261
private void applyStateToSelection(TextInputChannel.TextEditState state) {
@@ -220,6 +285,45 @@ private void setTextInputEditingState(View view, TextInputChannel.TextEditState
220285
}
221286

222287
private void clearTextInputClient() {
223-
mClient = 0;
288+
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
289+
// Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node
290+
// that lost focus may be notified before or after a node that gained focus.
291+
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text
292+
// field's focus node will be notified that it lost focus after the AndroidView was notified that it gained
293+
// focus. When this happens the text field will send a clearTextInput command which we ignore.
294+
// By doing this we prevent the framework from clearing a platform view input client(the only way to do so
295+
// is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views
296+
// text input client, and it may be error prone as we don't know how the platform view manages the input
297+
// connection and we probably shouldn't interfere.
298+
// If we ever want to allow the framework to clear a platform view text client we should probably consider
299+
// changing the focus manager such that focus nodes that lost focus are notified before focus nodes that
300+
// gained focus as part of the same focus event.
301+
return;
302+
}
303+
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
304+
unlockPlatformViewInputConnection();
305+
}
306+
307+
static private class InputTarget {
308+
enum Type {
309+
NO_TARGET,
310+
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework.
311+
FRAMEWORK_CLIENT,
312+
// InputConnection is managed by an embedded platform view.
313+
PLATFORM_VIEW
314+
}
315+
316+
public InputTarget(@NonNull Type type, int id) {
317+
this.type = type;
318+
this.id = id;
319+
}
320+
321+
@NonNull
322+
Type type;
323+
// The ID of the input target.
324+
//
325+
// For framework clients this is the framework input connection client ID.
326+
// For platform views this is the platform view's ID.
327+
int id;
224328
}
225329
}

shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
* Facilitates interaction between the accessibility bridge and embedded platform views.
1212
*/
1313
public interface PlatformViewsAccessibilityDelegate {
14-
1514
/**
1615
* Returns the root of the view hierarchy for the platform view with the requested id, or null if there is no
1716
* corresponding view.

0 commit comments

Comments
 (0)