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

Commit 6145e90

Browse files
Android Embedding PR 13: Integrated text input, keyevent input, and some other channel comms in FlutterView. (#7979)
1 parent 56c1615 commit 6145e90

File tree

3 files changed

+269
-9
lines changed

3 files changed

+269
-9
lines changed

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

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,31 @@
55
package io.flutter.embedding.engine.android;
66

77
import android.content.Context;
8+
import android.content.res.Configuration;
9+
import android.graphics.Rect;
10+
import android.os.Build;
11+
import android.os.LocaleList;
812
import android.support.annotation.NonNull;
913
import android.support.annotation.Nullable;
14+
import android.text.format.DateFormat;
1015
import android.util.AttributeSet;
1116
import android.util.Log;
17+
import android.view.KeyEvent;
18+
import android.view.MotionEvent;
19+
import android.view.WindowInsets;
20+
import android.view.inputmethod.EditorInfo;
21+
import android.view.inputmethod.InputConnection;
22+
import android.view.inputmethod.InputMethod;
23+
import android.view.inputmethod.InputMethodManager;
1224
import android.widget.FrameLayout;
1325

26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.Locale;
29+
1430
import io.flutter.embedding.engine.FlutterEngine;
1531
import io.flutter.embedding.engine.renderer.FlutterRenderer;
32+
import io.flutter.plugin.editing.TextInputPlugin;
1633

1734
/**
1835
* Displays a Flutter UI on an Android device.
@@ -50,6 +67,16 @@ public class FlutterView extends FrameLayout {
5067
@Nullable
5168
private FlutterEngine flutterEngine;
5269

70+
// Components that process various types of Android View input and events,
71+
// possibly storing intermediate state, and communicating those events to Flutter.
72+
//
73+
// These components essentially add some additional behavioral logic on top of
74+
// existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc.
75+
@Nullable
76+
private TextInputPlugin textInputPlugin;
77+
@Nullable
78+
private AndroidKeyProcessor androidKeyProcessor;
79+
5380
/**
5481
* Constructs a {@code FlutterSurfaceView} programmatically, without any XML attributes.
5582
*
@@ -103,6 +130,176 @@ private void init() {
103130
}
104131
}
105132

133+
//------- Start: Process View configuration that Flutter cares about. ------
134+
/**
135+
* Sends relevant configuration data from Android to Flutter when the Android
136+
* {@link Configuration} changes.
137+
*
138+
* The Android {@link Configuration} might change as a result of device orientation
139+
* change, device language change, device text scale factor change, etc.
140+
*/
141+
@Override
142+
protected void onConfigurationChanged(Configuration newConfig) {
143+
super.onConfigurationChanged(newConfig);
144+
sendLocalesToFlutter(newConfig);
145+
sendUserSettingsToFlutter();
146+
}
147+
148+
/**
149+
* Invoked when this {@code FlutterView} changes size, including upon initial
150+
* measure.
151+
*
152+
* The initial measure reports an {@code oldWidth} and {@code oldHeight} of zero.
153+
*
154+
* Flutter cares about the width and height of the view that displays it on the host
155+
* platform. Therefore, when this method is invoked, the new width and height are
156+
* communicated to Flutter as the "physical size" of the view that displays Flutter's
157+
* UI.
158+
*/
159+
@Override
160+
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
161+
// TODO(mattcarroll): hookup to viewport metrics.
162+
super.onSizeChanged(width, height, oldWidth, oldHeight);
163+
}
164+
165+
/**
166+
* Invoked when Android's desired window insets change, i.e., padding.
167+
*
168+
* Flutter does not use a standard {@code View} hierarchy and therefore Flutter is
169+
* unaware of these insets. Therefore, this method calculates the viewport metrics
170+
* that Flutter should use and then sends those metrics to Flutter.
171+
*
172+
* This callback is not present in API < 20, which means lower API devices will see
173+
* the wider than expected padding when the status and navigation bars are hidden.
174+
*/
175+
@Override
176+
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
177+
// TODO(mattcarroll): hookup to Flutter metrics.
178+
return insets;
179+
}
180+
181+
/**
182+
* Invoked when Android's desired window insets change, i.e., padding.
183+
*
184+
* {@code fitSystemWindows} is an earlier version of
185+
* {@link #onApplyWindowInsets(WindowInsets)}. See that method for more details
186+
* about how window insets relate to Flutter.
187+
*/
188+
@Override
189+
@SuppressWarnings("deprecation")
190+
protected boolean fitSystemWindows(Rect insets) {
191+
// TODO(mattcarroll): hookup to Flutter metrics.
192+
return super.fitSystemWindows(insets);
193+
}
194+
//------- End: Process View configuration that Flutter cares about. --------
195+
196+
//-------- Start: Process UI I/O that Flutter cares about. -------
197+
/**
198+
* Creates an {@link InputConnection} to work with a {@link android.view.inputmethod.InputMethodManager}.
199+
*
200+
* Any {@code View} that can take focus or process text input must implement this
201+
* method by returning a non-null {@code InputConnection}. Flutter may render one or
202+
* many focusable and text-input widgets, therefore {@code FlutterView} must support
203+
* an {@code InputConnection}.
204+
*
205+
* The {@code InputConnection} returned from this method comes from a
206+
* {@link TextInputPlugin}, which is owned by this {@code FlutterView}. A
207+
* {@link TextInputPlugin} exists to encapsulate the nuances of input communication,
208+
* rather than spread that logic throughout this {@code FlutterView}.
209+
*/
210+
@Override
211+
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
212+
if (!isAttachedToFlutterEngine()) {
213+
return super.onCreateInputConnection(outAttrs);
214+
}
215+
216+
return textInputPlugin.createInputConnection(this, outAttrs);
217+
}
218+
219+
/**
220+
* Invoked when key is released.
221+
*
222+
* This method is typically invoked in response to the release of a physical
223+
* keyboard key or a D-pad button. It is generally not invoked when a virtual
224+
* software keyboard is used, though a software keyboard may choose to invoke
225+
* this method in some situations.
226+
*
227+
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
228+
* may do some additional work with the given {@link KeyEvent}, e.g., combine this
229+
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
230+
* character.
231+
*/
232+
@Override
233+
public boolean onKeyUp(int keyCode, KeyEvent event) {
234+
if (!isAttachedToFlutterEngine()) {
235+
return super.onKeyUp(keyCode, event);
236+
}
237+
238+
androidKeyProcessor.onKeyUp(event);
239+
return super.onKeyUp(keyCode, event);
240+
}
241+
242+
/**
243+
* Invoked when key is pressed.
244+
*
245+
* This method is typically invoked in response to the press of a physical
246+
* keyboard key or a D-pad button. It is generally not invoked when a virtual
247+
* software keyboard is used, though a software keyboard may choose to invoke
248+
* this method in some situations.
249+
*
250+
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
251+
* may do some additional work with the given {@link KeyEvent}, e.g., combine this
252+
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
253+
* character.
254+
*/
255+
@Override
256+
public boolean onKeyDown(int keyCode, KeyEvent event) {
257+
if (!isAttachedToFlutterEngine()) {
258+
return super.onKeyDown(keyCode, event);
259+
}
260+
261+
androidKeyProcessor.onKeyDown(event);
262+
return super.onKeyDown(keyCode, event);
263+
}
264+
265+
/**
266+
* Invoked by Android when a user touch event occurs.
267+
*
268+
* Flutter handles all of its own gesture detection and processing, therefore this
269+
* method forwards all {@link MotionEvent} data from Android to Flutter.
270+
*/
271+
@Override
272+
public boolean onTouchEvent(MotionEvent event) {
273+
if (!isAttachedToFlutterEngine()) {
274+
return false;
275+
}
276+
277+
// TODO(mattcarroll): forward event to touch processore when it's merged in.
278+
return false;
279+
}
280+
281+
/**
282+
* Invoked by Android when a hover-compliant input system causes a hover event.
283+
*
284+
* An example of hover events is a stylus sitting near an Android screen. As the
285+
* stylus moves from outside a {@code View} to hover over a {@code View}, or move
286+
* around within a {@code View}, or moves from over a {@code View} to outside a
287+
* {@code View}, a corresponding {@link MotionEvent} is reported via this method.
288+
*
289+
* Hover events can be used for accessibility touch exploration and therefore are
290+
* processed here for accessibility purposes.
291+
*/
292+
@Override
293+
public boolean onHoverEvent(MotionEvent event) {
294+
if (!isAttachedToFlutterEngine()) {
295+
return false;
296+
}
297+
298+
// TODO(mattcarroll): hook up to accessibility.
299+
return false;
300+
}
301+
//-------- End: Process UI I/O that Flutter cares about. ---------
302+
106303
/**
107304
* Connects this {@code FlutterView} to the given {@link FlutterEngine}.
108305
*
@@ -129,6 +326,26 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
129326

130327
// Instruct our FlutterRenderer that we are now its designated RenderSurface.
131328
this.flutterEngine.getRenderer().attachToRenderSurface(renderSurface);
329+
330+
// Initialize various components that know how to process Android View I/O
331+
// in a way that Flutter understands.
332+
textInputPlugin = new TextInputPlugin(
333+
this,
334+
this.flutterEngine.getDartExecutor()
335+
);
336+
androidKeyProcessor = new AndroidKeyProcessor(
337+
this.flutterEngine.getKeyEventChannel(),
338+
textInputPlugin
339+
);
340+
341+
// Inform the Android framework that it should retrieve a new InputConnection
342+
// now that an engine is attached.
343+
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
344+
textInputPlugin.getInputMethodManager().restartInput(this);
345+
346+
// Push View and Context related information from Android to Flutter.
347+
sendUserSettingsToFlutter();
348+
sendLocalesToFlutter(getResources().getConfiguration());
132349
}
133350

134351
/**
@@ -147,6 +364,12 @@ public void detachFromFlutterEngine() {
147364
}
148365
Log.d(TAG, "Detaching from Flutter Engine");
149366

367+
// Inform the Android framework that it should retrieve a new InputConnection
368+
// now that the engine is detached. The new InputConnection will be null, which
369+
// signifies that this View does not process input (until a new engine is attached).
370+
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
371+
textInputPlugin.getInputMethodManager().restartInput(this);
372+
150373
// Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
151374
flutterEngine.getRenderer().detachFromRenderSurface();
152375
flutterEngine = null;
@@ -163,6 +386,42 @@ private boolean isAttachedToFlutterEngine() {
163386
return flutterEngine != null;
164387
}
165388

389+
/**
390+
* Send the current {@link Locale} configuration to Flutter.
391+
*
392+
* FlutterEngine must be non-null when this method is invoked.
393+
*/
394+
@SuppressWarnings("deprecation")
395+
private void sendLocalesToFlutter(Configuration config) {
396+
List<Locale> locales = new ArrayList<>();
397+
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
398+
LocaleList localeList = config.getLocales();
399+
int localeCount = localeList.size();
400+
for (int index = 0; index < localeCount; ++index) {
401+
Locale locale = localeList.get(index);
402+
locales.add(locale);
403+
}
404+
} else {
405+
locales.add(config.locale);
406+
}
407+
flutterEngine.getLocalizationChannel().sendLocales(locales);
408+
}
409+
410+
/**
411+
* Send various user preferences of this Android device to Flutter.
412+
*
413+
* For example, sends the user's "text scale factor" preferences, as well as the user's clock
414+
* format preference.
415+
*
416+
* FlutterEngine must be non-null when this method is invoked.
417+
*/
418+
private void sendUserSettingsToFlutter() {
419+
flutterEngine.getSettingsChannel().startMessage()
420+
.setTextScaleFactor(getResources().getConfiguration().fontScale)
421+
.setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
422+
.send();
423+
}
424+
166425
/**
167426
* Render modes for a {@link FlutterView}.
168427
*/

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
import android.text.Editable;
99
import android.text.Selection;
1010
import android.view.KeyEvent;
11+
import android.view.View;
1112
import android.view.inputmethod.BaseInputConnection;
1213
import android.view.inputmethod.EditorInfo;
1314
import android.view.inputmethod.InputMethodManager;
1415

1516
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
1617
import io.flutter.plugin.common.ErrorLogResult;
1718
import io.flutter.plugin.common.MethodChannel;
18-
import io.flutter.view.FlutterView;
1919

2020
class InputConnectionAdaptor extends BaseInputConnection {
21-
private final FlutterView mFlutterView;
21+
private final View mFlutterView;
2222
private final int mClient;
2323
private final TextInputChannel textInputChannel;
2424
private final Editable mEditable;
@@ -29,7 +29,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
2929
new ErrorLogResult("FlutterTextInput");
3030

3131
public InputConnectionAdaptor(
32-
FlutterView view,
32+
View view,
3333
int client,
3434
TextInputChannel textInputChannel,
3535
Editable editable

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import android.text.Editable;
1111
import android.text.InputType;
1212
import android.text.Selection;
13+
import android.view.View;
1314
import android.view.inputmethod.BaseInputConnection;
1415
import android.view.inputmethod.EditorInfo;
1516
import android.view.inputmethod.InputConnection;
@@ -24,7 +25,7 @@
2425
*/
2526
public class TextInputPlugin {
2627
@NonNull
27-
private final FlutterView mView;
28+
private final View mView;
2829
@NonNull
2930
private final InputMethodManager mImm;
3031
@NonNull
@@ -38,7 +39,7 @@ public class TextInputPlugin {
3839
@Nullable
3940
private InputConnection lastInputConnection;
4041

41-
public TextInputPlugin(FlutterView view, @NonNull DartExecutor dartExecutor) {
42+
public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor) {
4243
mView = view;
4344
mImm = (InputMethodManager) view.getContext().getSystemService(
4445
Context.INPUT_METHOD_SERVICE);
@@ -126,7 +127,7 @@ private static int inputTypeFromTextInputType(
126127
return textType;
127128
}
128129

129-
public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs) {
130+
public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
130131
if (mClient == 0) {
131132
lastInputConnection = null;
132133
return lastInputConnection;
@@ -173,12 +174,12 @@ public InputConnection getLastInputConnection() {
173174
return lastInputConnection;
174175
}
175176

176-
private void showTextInput(FlutterView view) {
177+
private void showTextInput(View view) {
177178
view.requestFocus();
178179
mImm.showSoftInput(view, 0);
179180
}
180181

181-
private void hideTextInput(FlutterView view) {
182+
private void hideTextInput(View view) {
182183
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
183184
}
184185

@@ -203,7 +204,7 @@ private void applyStateToSelection(TextInputChannel.TextEditState state) {
203204
}
204205
}
205206

206-
private void setTextInputEditingState(FlutterView view, TextInputChannel.TextEditState state) {
207+
private void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
207208
if (!mRestartInputPending && state.text.equals(mEditable.toString())) {
208209
applyStateToSelection(state);
209210
mImm.updateSelection(mView, Math.max(Selection.getSelectionStart(mEditable), 0),

0 commit comments

Comments
 (0)