Simplify state management in the Android text editing plugin (flutter/engine#3769)
In particular, this avoids some unnecessary calls to InputMethodManager.restartInput that caused noticeable lag when moving the cursor. Fixes https://github.com/flutter/flutter/issues/9928
This commit is contained in:
@@ -4,9 +4,11 @@
|
||||
|
||||
package io.flutter.plugin.editing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.Selection;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
@@ -17,63 +19,104 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class InputConnectionAdaptor extends BaseInputConnection {
|
||||
private final FlutterView mFlutterView;
|
||||
private final int mClient;
|
||||
private final TextInputPlugin mPlugin;
|
||||
private final MethodChannel mFlutterChannel;
|
||||
private final Map<String, Object> mOutgoingState;
|
||||
private final Editable mEditable;
|
||||
private int mBatchCount;
|
||||
private InputMethodManager mImm;
|
||||
|
||||
public InputConnectionAdaptor(FlutterView view, int client,
|
||||
TextInputPlugin plugin, MethodChannel flutterChannel) {
|
||||
MethodChannel flutterChannel, Editable editable) {
|
||||
super(view, true);
|
||||
mFlutterView = view;
|
||||
mClient = client;
|
||||
mPlugin = plugin;
|
||||
mFlutterChannel = flutterChannel;
|
||||
mOutgoingState = new HashMap<>();
|
||||
mEditable = editable;
|
||||
mBatchCount = 0;
|
||||
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
// Send the current state of the editable to Flutter.
|
||||
private void updateEditingState() {
|
||||
final Editable content = getEditable();
|
||||
mOutgoingState.put("text", content.toString());
|
||||
mOutgoingState.put("selectionBase", Selection.getSelectionStart(content));
|
||||
mOutgoingState.put("selectionExtent", Selection.getSelectionEnd(content));
|
||||
mOutgoingState.put("composingBase", BaseInputConnection.getComposingSpanStart(content));
|
||||
mOutgoingState.put("composingExtent", BaseInputConnection.getComposingSpanEnd(content));
|
||||
mFlutterChannel.invokeMethod("TextInputClient.updateEditingState", Arrays
|
||||
.asList(mClient, mOutgoingState));
|
||||
mPlugin.setLatestEditingState(mOutgoingState);
|
||||
// If the IME is in the middle of a batch edit, then wait until it completes.
|
||||
if (mBatchCount > 0)
|
||||
return;
|
||||
|
||||
int selectionStart = Selection.getSelectionStart(mEditable);
|
||||
int selectionEnd = Selection.getSelectionEnd(mEditable);
|
||||
int composingStart = BaseInputConnection.getComposingSpanStart(mEditable);
|
||||
int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable);
|
||||
|
||||
mImm.updateSelection(mFlutterView,
|
||||
selectionStart, selectionEnd,
|
||||
composingStart, composingEnd);
|
||||
|
||||
HashMap<Object, Object> state = new HashMap<Object, Object>();
|
||||
state.put("text", mEditable.toString());
|
||||
state.put("selectionBase", selectionStart);
|
||||
state.put("selectionExtent", selectionEnd);
|
||||
state.put("composingBase", composingStart);
|
||||
state.put("composingExtent", composingEnd);
|
||||
mFlutterChannel.invokeMethod("TextInputClient.updateEditingState",
|
||||
Arrays.asList(mClient, state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginBatchEdit() {
|
||||
mBatchCount++;
|
||||
return super.beginBatchEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endBatchEdit() {
|
||||
boolean result = super.endBatchEdit();
|
||||
mBatchCount--;
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
final boolean result = super.commitText(text, newCursorPosition);
|
||||
boolean result = super.commitText(text, newCursorPosition);
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
final boolean result = super.deleteSurroundingText(beforeLength, afterLength);
|
||||
boolean result = super.deleteSurroundingText(beforeLength, afterLength);
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingRegion(int start, int end) {
|
||||
final boolean result = super.setComposingRegion(start, end);
|
||||
boolean result = super.setComposingRegion(start, end);
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
final boolean result = super.setComposingText(text, newCursorPosition);
|
||||
boolean result;
|
||||
if (text.length() == 0) {
|
||||
result = super.commitText(text, newCursorPosition);
|
||||
} else {
|
||||
result = super.setComposingText(text, newCursorPosition);
|
||||
}
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setSelection(int start, int end) {
|
||||
final boolean result = super.setSelection(start, end);
|
||||
boolean result = super.setSelection(start, end);
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
@@ -82,26 +125,26 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
final boolean result = super.sendKeyEvent(event);
|
||||
if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
// Weird special case. This method is (sometimes) called for the backspace key in 2
|
||||
// situations:
|
||||
// 1. There is no selection. In that case, we want to delete the previous character.
|
||||
// 2. There is a selection. In that case, we want to delete the selection.
|
||||
// event.getNumber() is 0, and commitText("", 1) will do what we want.
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL &&
|
||||
optInt("selectionBase", -1) == optInt("selectionExtent", -1)) {
|
||||
deleteSurroundingText(1, 0);
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
||||
int selStart = Selection.getSelectionStart(mEditable);
|
||||
int selEnd = Selection.getSelectionEnd(mEditable);
|
||||
if (selEnd > selStart) {
|
||||
// Delete the selection.
|
||||
Selection.setSelection(mEditable, selStart);
|
||||
deleteSurroundingText(0, selEnd - selStart);
|
||||
} else if (selStart > 0) {
|
||||
// Delete to the left of the cursor.
|
||||
Selection.setSelection(mEditable, selStart - 1);
|
||||
deleteSurroundingText(0, 1);
|
||||
}
|
||||
} else {
|
||||
String text = event.getNumber() == 0 ? "" : String.valueOf(event.getNumber());
|
||||
commitText(text, 1);
|
||||
// Enter a character.
|
||||
commitText(String.valueOf(event.getNumber()), 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int optInt(String key, int defaultValue) {
|
||||
return mOutgoingState.containsKey(key) ? (Integer) mOutgoingState.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performEditorAction(int actionCode) {
|
||||
// TODO(abarth): Support more actions.
|
||||
|
||||
@@ -6,7 +6,10 @@ package io.flutter.plugin.editing;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@@ -29,16 +32,16 @@ import org.json.JSONObject;
|
||||
*/
|
||||
public class TextInputPlugin implements MethodCallHandler {
|
||||
|
||||
private final Activity mActivity;
|
||||
private final FlutterView mView;
|
||||
private final InputMethodManager mImm;
|
||||
private final MethodChannel mFlutterChannel;
|
||||
private int mClient = 0;
|
||||
private JSONObject mConfiguration;
|
||||
private JSONObject mLatestState;
|
||||
private Editable mEditable;
|
||||
|
||||
public TextInputPlugin(Activity activity, FlutterView view) {
|
||||
mActivity = activity;
|
||||
public TextInputPlugin(FlutterView view) {
|
||||
mView = view;
|
||||
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
mFlutterChannel = new MethodChannel(view, "flutter/textinput",
|
||||
JSONMethodCodec.INSTANCE);
|
||||
mFlutterChannel.setMethodCallHandler(this);
|
||||
@@ -98,60 +101,53 @@ public class TextInputPlugin implements MethodCallHandler {
|
||||
throws JSONException {
|
||||
if (mClient == 0)
|
||||
return null;
|
||||
|
||||
outAttrs.inputType = inputTypeFromTextInputType(mConfiguration.getString("inputType"),
|
||||
mConfiguration.optBoolean("obscureText"));
|
||||
if (!mConfiguration.isNull("actionLabel"))
|
||||
outAttrs.actionLabel = mConfiguration.getString("actionLabel");
|
||||
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
InputConnectionAdaptor connection = new InputConnectionAdaptor(view, mClient, this,
|
||||
mFlutterChannel);
|
||||
if (mLatestState != null) {
|
||||
int selectionBase = (Integer) mLatestState.get("selectionBase");
|
||||
int selectionExtent = (Integer) mLatestState.get("selectionExtent");
|
||||
outAttrs.initialSelStart = selectionBase;
|
||||
outAttrs.initialSelEnd = selectionExtent;
|
||||
connection.getEditable().append((String) mLatestState.get("text"));
|
||||
connection.setSelection(Math.max(selectionBase, 0),
|
||||
Math.max(selectionExtent, 0));
|
||||
connection.setComposingRegion((Integer) mLatestState.get("composingBase"),
|
||||
(Integer) mLatestState.get("composingExtent"));
|
||||
} else {
|
||||
outAttrs.initialSelStart = 0;
|
||||
outAttrs.initialSelEnd = 0;
|
||||
}
|
||||
|
||||
InputConnectionAdaptor connection = new InputConnectionAdaptor(view, mClient, mFlutterChannel, mEditable);
|
||||
outAttrs.initialSelStart = Math.max(Selection.getSelectionStart(mEditable), 0);
|
||||
outAttrs.initialSelEnd = Math.max(Selection.getSelectionEnd(mEditable), 0);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private void showTextInput(FlutterView view) {
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, 0);
|
||||
mImm.showSoftInput(view, 0);
|
||||
}
|
||||
|
||||
private void hideTextInput(FlutterView view) {
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
|
||||
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
|
||||
}
|
||||
|
||||
private void setTextInputClient(FlutterView view, int client, JSONObject configuration) {
|
||||
mLatestState = null;
|
||||
mClient = client;
|
||||
mConfiguration = configuration;
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.restartInput(view);
|
||||
mEditable = Editable.Factory.getInstance().newEditable("");
|
||||
|
||||
mImm.restartInput(view);
|
||||
}
|
||||
|
||||
private void setTextInputEditingState(FlutterView view, JSONObject state) {
|
||||
mLatestState = state;
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.restartInput(view);
|
||||
}
|
||||
|
||||
void setLatestEditingState(Map<String, Object> state) {
|
||||
mLatestState = (JSONObject) JSONUtil.wrap(state);
|
||||
private void setTextInputEditingState(FlutterView view, JSONObject state)
|
||||
throws JSONException {
|
||||
if (state.getString("text").equals(mEditable.toString())) {
|
||||
Selection.setSelection(mEditable, state.getInt("selectionBase"),
|
||||
state.getInt("selectionExtent"));
|
||||
mImm.updateSelection(
|
||||
mView,
|
||||
Math.max(Selection.getSelectionStart(mEditable), 0),
|
||||
Math.max(Selection.getSelectionEnd(mEditable), 0),
|
||||
BaseInputConnection.getComposingSpanStart(mEditable),
|
||||
BaseInputConnection.getComposingSpanEnd(mEditable));
|
||||
} else {
|
||||
mEditable.replace(0, mEditable.length(), state.getString("text"));
|
||||
Selection.setSelection(mEditable, state.getInt("selectionBase"),
|
||||
state.getInt("selectionExtent"));
|
||||
mImm.restartInput(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearTextInputClient() {
|
||||
|
||||
@@ -174,7 +174,7 @@ public class FlutterView extends SurfaceView
|
||||
"flutter/platform", JSONMethodCodec.INSTANCE);
|
||||
flutterPlatformChannel.setMethodCallHandler(platformPlugin);
|
||||
addActivityLifecycleListener(platformPlugin);
|
||||
mTextInputPlugin = new TextInputPlugin((Activity) getContext(), this);
|
||||
mTextInputPlugin = new TextInputPlugin(this);
|
||||
|
||||
setLocale(getResources().getConfiguration().locale);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user