-
Notifications
You must be signed in to change notification settings - Fork 6k
TextEditingDelta support for Android #28175
Changes from all commits
6684c9e
a15925b
a56f86b
bf0ae8e
f2cc57b
6c5d5cf
9e5a8c1
b15a970
f3be317
65f0f71
c87b2b4
34ed4cd
e9742f8
e0278c9
0bca5e9
fd84d6a
a608ee3
e182d61
d2097ad
6f96926
89c4773
d436a4f
8c4ef06
92e30c9
f7858a1
6192ca4
7791598
5ff5114
71086dc
b717cb7
3d24083
8e78152
e6bb58a
327403d
31c188d
c4bb736
8e8e29e
f322d13
0c7c3a9
a5cc483
8a432f2
174bc56
1f9b195
4a3f1e2
794f288
2cd3260
b5bcd1b
7c5c287
5d3468f
e4430c2
11d6ddd
eb94a62
a204519
e6b86eb
1580b3d
2d1655a
d7a8800
2c60053
b7ac52c
c66555d
73c849e
b54b8b1
9496733
07f0c1d
2326d7b
510ca19
14c5f12
12da161
8d34321
f858e9f
e493f6e
7d2c858
d0659f0
7bacbed
88f1042
034f0e8
630b505
81232af
4b25c8e
1cd7aa1
9d7b337
72ab127
4a13b0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ void didChangeEditingState( | |
| private int mChangeNotificationDepth = 0; | ||
| private ArrayList<EditingStateWatcher> mListeners = new ArrayList<>(); | ||
| private ArrayList<EditingStateWatcher> mPendingListeners = new ArrayList<>(); | ||
| private ArrayList<TextEditingDelta> mBatchTextEditingDeltas = new ArrayList<>(); | ||
|
|
||
| private String mToStringCache; | ||
|
|
||
|
|
@@ -70,6 +71,17 @@ public Editable getEditable() { | |
| }; | ||
| } | ||
|
|
||
| public ArrayList<TextEditingDelta> extractBatchTextEditingDeltas() { | ||
| ArrayList<TextEditingDelta> currentBatchDeltas = | ||
| new ArrayList<TextEditingDelta>(mBatchTextEditingDeltas); | ||
| mBatchTextEditingDeltas.clear(); | ||
| return currentBatchDeltas; | ||
| } | ||
|
|
||
| public void clearBatchDeltas() { | ||
| mBatchTextEditingDeltas.clear(); | ||
| } | ||
|
|
||
| /// Starts a new batch edit during which change notifications will be put on hold until all batch | ||
| /// edits end. | ||
| /// | ||
|
|
@@ -142,7 +154,13 @@ public void setEditingState(TextInputChannel.TextEditState newState) { | |
| } else { | ||
| Selection.removeSelection(this); | ||
| } | ||
|
|
||
| setComposingRange(newState.composingStart, newState.composingEnd); | ||
|
|
||
| // Updates from the framework should not have a delta created for it as they have already been | ||
| // applied on the framework side. | ||
| clearBatchDeltas(); | ||
|
|
||
| endBatchEdit(); | ||
| } | ||
|
|
||
|
|
@@ -179,6 +197,8 @@ public SpannableStringBuilder replace( | |
| Log.e(TAG, "editing state should not be changed in a listener callback"); | ||
| } | ||
|
|
||
| final CharSequence oldText = toString(); | ||
|
|
||
| boolean textChanged = end - start != tbend - tbstart; | ||
| for (int i = 0; i < end - start && !textChanged; i++) { | ||
| textChanged |= charAt(start + i) != tb.charAt(tbstart + i); | ||
|
|
@@ -193,6 +213,17 @@ public SpannableStringBuilder replace( | |
| final int composingEnd = getComposingEnd(); | ||
|
|
||
| final SpannableStringBuilder editable = super.replace(start, end, tb, tbstart, tbend); | ||
| mBatchTextEditingDeltas.add( | ||
| new TextEditingDelta( | ||
| oldText, | ||
| start, | ||
| end, | ||
| tb, | ||
| getSelectionStart(), | ||
| getSelectionEnd(), | ||
| getComposingStart(), | ||
| getComposingEnd())); | ||
|
|
||
| if (mBatchEditNestDepth > 0) { | ||
| return editable; | ||
| } | ||
|
|
@@ -240,6 +271,20 @@ public final int getComposingEnd() { | |
| return BaseInputConnection.getComposingSpanEnd(this); | ||
| } | ||
|
|
||
| @Override | ||
| public void setSpan(Object what, int start, int end, int flags) { | ||
| super.setSpan(what, start, end, flags); | ||
| // Setting a span does not involve mutating the text value in the editing state. Here we create | ||
| // a non text update delta with any updated selection and composing regions. | ||
| mBatchTextEditingDeltas.add( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible but it shouldn't affect the final result since they are
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The batch may look something like this if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally there should be no overlap between consecutive deltas and there should be no redundant deltas. Should be fine for now. |
||
| new TextEditingDelta( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make a "nonTextUpdate" delta be its own constructor. It is not always obvious to pass
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense, I do this already in the iOS PR. |
||
| toString(), | ||
| getSelectionStart(), | ||
| getSelectionEnd(), | ||
| getComposingStart(), | ||
| getComposingEnd())); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return mToStringCache != null ? mToStringCache : (mToStringCache = super.toString()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| package io.flutter.plugin.editing; | ||
|
|
||
| import androidx.annotation.VisibleForTesting; | ||
| import io.flutter.Log; | ||
| import org.json.JSONException; | ||
| import org.json.JSONObject; | ||
|
|
||
| /// A representation of the change that occured to an editing state, along with the resulting | ||
| /// composing and selection regions. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we explicitly differentiate between text content changes and selection/composing region index only changes? However it is handled, it should be documented.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never mind, that is under the umbrella of TextEditingDeltaNonTextUpdate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the framework we do, but on the engine there is no differentiation. This differentiation is documented on the framework side, should it also have something here? Edit: didn't see your comment before this, disregard. |
||
| public final class TextEditingDelta { | ||
| private CharSequence oldText; | ||
| private CharSequence deltaText; | ||
| private int deltaStart; | ||
| private int deltaEnd; | ||
| private int newSelectionStart; | ||
| private int newSelectionEnd; | ||
| private int newComposingStart; | ||
| private int newComposingEnd; | ||
|
|
||
| private static final String TAG = "TextEditingDelta"; | ||
|
|
||
| public TextEditingDelta( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was expecting that we can just extract the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That wouldn't work in some cases. Say the case where we are typing "world". Current state: "worl|" or for a deletion Current state: "world|" If typing at the end of the composing region the text in the composing region is replaced with the entire new composing text. If we move the cursor inside the word currently being composed like below we get this call. Here we are actually given the insertion point and the character inserted, where when it is at the end of the composing region we must do some reasoning. On iOS we have something slightly different as well. Current state: "world|" another thing iOS does is when typing with a CJK keyboard it will exhibit behavior similar to Android composing region where the entire composing region is replaced on input. Because these values can vary a bit by platform for the same operation I think it is important to unify them into one format. I'm unsure if a replacement of "world" with "worl" would have the same result as replacing "d" in "world" with an empty string regarding if ranges of "world" where bolded/styled. Would these ranges still have the information needed to contract/expand as expected?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah makes sense, thank you for the clarification! Do you think we'll be able to extract that info directly from the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately from my research of the API I don't think so. Even the
Maybe I missed something but this is my conclusion after my research.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. So
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer a bit more documentation in this class. If this were the entrypoint for someone learning about this feature, the documentation here is a bit lacking to properly introduce what this does and find the rest of the code. |
||
| CharSequence oldEditable, | ||
| int replacementDestinationStart, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since in this system, we have a Delta type called Also, what is the difference between that testing constructor and this one? They seem to do the same thing, but parameter naming is out of sync.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I don't call it Your right the testing constructor can be eliminated. |
||
| int replacementDestinationEnd, | ||
| CharSequence replacementSource, | ||
| int selectionStart, | ||
| int selectionEnd, | ||
| int composingStart, | ||
| int composingEnd) { | ||
| newSelectionStart = selectionStart; | ||
| newSelectionEnd = selectionEnd; | ||
| newComposingStart = composingStart; | ||
| newComposingEnd = composingEnd; | ||
|
|
||
| setDeltas( | ||
| oldEditable, | ||
| replacementSource.toString(), | ||
| replacementDestinationStart, | ||
| replacementDestinationEnd); | ||
| } | ||
|
|
||
| // Non text update delta constructor. | ||
| public TextEditingDelta( | ||
| CharSequence oldText, | ||
| int selectionStart, | ||
| int selectionEnd, | ||
| int composingStart, | ||
| int composingEnd) { | ||
| newSelectionStart = selectionStart; | ||
| newSelectionEnd = selectionEnd; | ||
| newComposingStart = composingStart; | ||
| newComposingEnd = composingEnd; | ||
|
|
||
| setDeltas(oldText, "", -1, -1); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public CharSequence getOldText() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need these getters? The text input plugin only need to be able to build deltas and serialize them to JSON right? Are these for writing tests?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes there are now purely for tests.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe write a comment or comments stating that these are for testing only.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know java too well but do you need expose them as
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think we do. I just tried to switch them to private and it complained. |
||
| return oldText; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public CharSequence getDeltaText() { | ||
| return deltaText; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getDeltaStart() { | ||
| return deltaStart; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getDeltaEnd() { | ||
| return deltaEnd; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getNewSelectionStart() { | ||
| return newSelectionStart; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getNewSelectionEnd() { | ||
| return newSelectionEnd; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getNewComposingStart() { | ||
| return newComposingStart; | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| public int getNewComposingEnd() { | ||
| return newComposingEnd; | ||
| } | ||
|
|
||
| private void setDeltas(CharSequence oldText, CharSequence newText, int newStart, int newExtent) { | ||
| this.oldText = oldText; | ||
| deltaText = newText; | ||
| deltaStart = newStart; | ||
| deltaEnd = newExtent; | ||
| } | ||
|
|
||
| public JSONObject toJSON() { | ||
| JSONObject delta = new JSONObject(); | ||
|
|
||
| try { | ||
| delta.put("oldText", oldText.toString()); | ||
| delta.put("deltaText", deltaText.toString()); | ||
| delta.put("deltaStart", deltaStart); | ||
| delta.put("deltaEnd", deltaEnd); | ||
| delta.put("selectionBase", newSelectionStart); | ||
| delta.put("selectionExtent", newSelectionEnd); | ||
| delta.put("composingBase", newComposingStart); | ||
| delta.put("composingExtent", newComposingEnd); | ||
| } catch (JSONException e) { | ||
| Log.e(TAG, "unable to create JSONObject: " + e); | ||
| } | ||
|
|
||
| return delta; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| import io.flutter.embedding.engine.systemchannels.TextInputChannel; | ||
| import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState; | ||
| import io.flutter.plugin.platform.PlatformViewsController; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
|
|
||
| /** Android implementation of the text input plugin. */ | ||
|
|
@@ -624,6 +625,9 @@ public void didChangeEditingState( | |
| final int selectionEnd = mEditable.getSelectionEnd(); | ||
| final int composingStart = mEditable.getComposingStart(); | ||
| final int composingEnd = mEditable.getComposingEnd(); | ||
|
|
||
| final ArrayList<TextEditingDelta> batchTextEditingDeltas = | ||
| mEditable.extractBatchTextEditingDeltas(); | ||
| final boolean skipFrameworkUpdate = | ||
| // The framework needs to send its editing state first. | ||
| mLastKnownFrameworkTextEditingState == null | ||
|
|
@@ -634,16 +638,25 @@ public void didChangeEditingState( | |
| && composingEnd == mLastKnownFrameworkTextEditingState.composingEnd); | ||
| if (!skipFrameworkUpdate) { | ||
| Log.v(TAG, "send EditingState to flutter: " + mEditable.toString()); | ||
| textInputChannel.updateEditingState( | ||
| inputTarget.id, | ||
| mEditable.toString(), | ||
| selectionStart, | ||
| selectionEnd, | ||
| composingStart, | ||
| composingEnd); | ||
|
|
||
| if (configuration.enableDeltaModel) { | ||
| textInputChannel.updateEditingStateWithDeltas(inputTarget.id, batchTextEditingDeltas); | ||
| mEditable.clearBatchDeltas(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This clearBatchDeltas makes sense! |
||
| } else { | ||
| textInputChannel.updateEditingState( | ||
| inputTarget.id, | ||
| mEditable.toString(), | ||
| selectionStart, | ||
| selectionEnd, | ||
| composingStart, | ||
| composingEnd); | ||
| } | ||
| mLastKnownFrameworkTextEditingState = | ||
| new TextEditState( | ||
| mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd); | ||
| } else { | ||
| // Don't accumulate deltas if they are not sent to the framework. | ||
| mEditable.clearBatchDeltas(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -817,7 +830,6 @@ public void autofill(SparseArray<AutofillValue> values) { | |
| editingValues.put(autofill.uniqueIdentifier, newState); | ||
| } | ||
| } | ||
|
|
||
| textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues); | ||
| } | ||
| // -------- End: Autofill ------- | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.