Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
ListenableEditingState.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.plugin.editing;
6
7import android.text.Editable;
8import android.text.Selection;
9import android.text.SpannableStringBuilder;
10import android.view.View;
11import android.view.inputmethod.BaseInputConnection;
12import androidx.annotation.NonNull;
13import androidx.annotation.Nullable;
14import io.flutter.Log;
15import io.flutter.embedding.engine.systemchannels.TextInputChannel;
16import java.util.ArrayList;
17
18/// The current editing state (text, selection range, composing range) the text input plugin holds.
19///
20/// As the name implies, this class also notifies its listeners when the editing state changes. When
21/// there're ongoing batch edits, change notifications will be deferred until all batch edits end
22/// (i.e. when the outermost batch edit ends). Listeners added during a batch edit will always be
23/// notified when all batch edits end, even if there's no real change.
24///
25/// Adding/removing listeners or changing the editing state in a didChangeEditingState callback may
26/// cause unexpected behavior.
27//
28// Currently this class does not notify its listeners on spans-only changes (e.g.,
29// Selection.setSelection). Wrap them in a batch edit to trigger a change notification.
30class ListenableEditingState extends SpannableStringBuilder {
32 // Changing the editing state in a didChangeEditingState callback may cause unexpected
33 // behavior.
35 boolean textChanged, boolean selectionChanged, boolean composingRegionChanged);
36 }
37
38 private static final String TAG = "ListenableEditingState";
39
40 private int mBatchEditNestDepth = 0;
41 // We don't support adding/removing listeners, or changing the editing state in a listener
42 // callback for now.
43 private int mChangeNotificationDepth = 0;
44 private ArrayList<EditingStateWatcher> mListeners = new ArrayList<>();
45 private ArrayList<EditingStateWatcher> mPendingListeners = new ArrayList<>();
46 private ArrayList<TextEditingDelta> mBatchTextEditingDeltas = new ArrayList<>();
47
48 private String mToStringCache;
49
50 private String mTextWhenBeginBatchEdit;
51 private int mSelectionStartWhenBeginBatchEdit;
52 private int mSelectionEndWhenBeginBatchEdit;
53 private int mComposingStartWhenBeginBatchEdit;
54 private int mComposingEndWhenBeginBatchEdit;
55
56 private BaseInputConnection mDummyConnection;
57
58 // The View is only used for creating a dummy BaseInputConnection for setComposingRegion. The View
59 // needs to have a non-null Context.
61 @Nullable TextInputChannel.TextEditState initialState, @NonNull View view) {
62 super();
63
64 Editable self = this;
65 mDummyConnection =
66 new BaseInputConnection(view, true) {
67 @Override
68 public Editable getEditable() {
69 return self;
70 }
71 };
72
73 if (initialState != null) {
74 setEditingState(initialState);
75 }
76 }
77
78 public ArrayList<TextEditingDelta> extractBatchTextEditingDeltas() {
79 ArrayList<TextEditingDelta> currentBatchDeltas =
80 new ArrayList<TextEditingDelta>(mBatchTextEditingDeltas);
81 mBatchTextEditingDeltas.clear();
82 return currentBatchDeltas;
83 }
84
85 public void clearBatchDeltas() {
86 mBatchTextEditingDeltas.clear();
87 }
88
89 /// Starts a new batch edit during which change notifications will be put on hold until all batch
90 /// edits end.
91 ///
92 /// Batch edits nest.
93 public void beginBatchEdit() {
94 mBatchEditNestDepth++;
95 if (mChangeNotificationDepth > 0) {
96 Log.e(TAG, "editing state should not be changed in a listener callback");
97 }
98 if (mBatchEditNestDepth == 1 && !mListeners.isEmpty()) {
99 mTextWhenBeginBatchEdit = toString();
100 mSelectionStartWhenBeginBatchEdit = getSelectionStart();
101 mSelectionEndWhenBeginBatchEdit = getSelectionEnd();
102 mComposingStartWhenBeginBatchEdit = getComposingStart();
103 mComposingEndWhenBeginBatchEdit = getComposingEnd();
104 }
105 }
106
107 /// Ends the current batch edit and flush pending change notifications if the current batch edit
108 /// is not nested (i.e. it is the last ongoing batch edit).
109 public void endBatchEdit() {
110 if (mBatchEditNestDepth == 0) {
111 Log.e(TAG, "endBatchEdit called without a matching beginBatchEdit");
112 return;
113 }
114 if (mBatchEditNestDepth == 1) {
115 for (final EditingStateWatcher listener : mPendingListeners) {
116 notifyListener(listener, true, true, true);
117 }
118
119 if (!mListeners.isEmpty()) {
120 Log.v(TAG, "didFinishBatchEdit with " + String.valueOf(mListeners.size()) + " listener(s)");
121 final boolean textChanged = !toString().equals(mTextWhenBeginBatchEdit);
122 final boolean selectionChanged =
123 mSelectionStartWhenBeginBatchEdit != getSelectionStart()
124 || mSelectionEndWhenBeginBatchEdit != getSelectionEnd();
125 final boolean composingRegionChanged =
126 mComposingStartWhenBeginBatchEdit != getComposingStart()
127 || mComposingEndWhenBeginBatchEdit != getComposingEnd();
128
129 notifyListenersIfNeeded(textChanged, selectionChanged, composingRegionChanged);
130 }
131 }
132
133 mListeners.addAll(mPendingListeners);
134 mPendingListeners.clear();
135 mBatchEditNestDepth--;
136 }
137
138 /// Update the composing region of the current editing state.
139 ///
140 /// If the range is invalid or empty, the current composing region will be removed.
141 public void setComposingRange(int composingStart, int composingEnd) {
142 if (composingStart < 0 || composingStart >= composingEnd) {
143 BaseInputConnection.removeComposingSpans(this);
144 } else {
145 mDummyConnection.setComposingRegion(composingStart, composingEnd);
146 }
147 }
148
149 /// Called when the framework sends updates to the text input plugin.
150 ///
151 /// This method will also update the composing region if it has changed.
152 public void setEditingState(TextInputChannel.TextEditState newState) {
154 replace(0, length(), newState.text);
155
156 if (newState.hasSelection()) {
157 Selection.setSelection(this, newState.selectionStart, newState.selectionEnd);
158 } else {
159 Selection.removeSelection(this);
160 }
161
162 setComposingRange(newState.composingStart, newState.composingEnd);
163
164 // Updates from the framework should not have a delta created for it as they have already been
165 // applied on the framework side.
167
168 endBatchEdit();
169 }
170
172 if (mChangeNotificationDepth > 0) {
173 Log.e(TAG, "adding a listener " + listener.toString() + " in a listener callback");
174 }
175 // It is possible for a listener to get added during a batch edit. When that happens we always
176 // notify the new listeners.
177 // This does not check if the listener is already in the list of existing listeners.
178 if (mBatchEditNestDepth > 0) {
179 Log.w(TAG, "a listener was added to EditingState while a batch edit was in progress");
180 mPendingListeners.add(listener);
181 } else {
182 mListeners.add(listener);
183 }
184 }
185
187 if (mChangeNotificationDepth > 0) {
188 Log.e(TAG, "removing a listener " + listener.toString() + " in a listener callback");
189 }
190 mListeners.remove(listener);
191 if (mBatchEditNestDepth > 0) {
192 mPendingListeners.remove(listener);
193 }
194 }
195
196 @Override
197 public SpannableStringBuilder replace(
198 int start, int end, CharSequence tb, int tbstart, int tbend) {
199
200 if (mChangeNotificationDepth > 0) {
201 Log.e(TAG, "editing state should not be changed in a listener callback");
202 }
203
204 final CharSequence oldText = toString();
205
206 boolean textChanged = end - start != tbend - tbstart;
207 for (int i = 0; i < end - start && !textChanged; i++) {
208 textChanged |= charAt(start + i) != tb.charAt(tbstart + i);
209 }
210 if (textChanged) {
211 mToStringCache = null;
212 }
213
214 final int selectionStart = getSelectionStart();
215 final int selectionEnd = getSelectionEnd();
216 final int composingStart = getComposingStart();
217 final int composingEnd = getComposingEnd();
218
219 final SpannableStringBuilder editable = super.replace(start, end, tb, tbstart, tbend);
220 mBatchTextEditingDeltas.add(
222 oldText,
223 start,
224 end,
225 tb,
229 getComposingEnd()));
230
231 if (mBatchEditNestDepth > 0) {
232 return editable;
233 }
234
235 final boolean selectionChanged =
236 getSelectionStart() != selectionStart || getSelectionEnd() != selectionEnd;
237 final boolean composingRegionChanged =
238 getComposingStart() != composingStart || getComposingEnd() != composingEnd;
239 notifyListenersIfNeeded(textChanged, selectionChanged, composingRegionChanged);
240 return editable;
241 }
242
243 private void notifyListener(
244 EditingStateWatcher listener,
245 boolean textChanged,
246 boolean selectionChanged,
247 boolean composingChanged) {
248 mChangeNotificationDepth++;
249 listener.didChangeEditingState(textChanged, selectionChanged, composingChanged);
250 mChangeNotificationDepth--;
251 }
252
253 private void notifyListenersIfNeeded(
254 boolean textChanged, boolean selectionChanged, boolean composingChanged) {
255 if (textChanged || selectionChanged || composingChanged) {
256 for (final EditingStateWatcher listener : mListeners) {
257 notifyListener(listener, textChanged, selectionChanged, composingChanged);
258 }
259 }
260 }
261
262 public final int getSelectionStart() {
263 return Selection.getSelectionStart(this);
264 }
265
266 public final int getSelectionEnd() {
267 return Selection.getSelectionEnd(this);
268 }
269
270 public final int getComposingStart() {
271 return BaseInputConnection.getComposingSpanStart(this);
272 }
273
274 public final int getComposingEnd() {
275 return BaseInputConnection.getComposingSpanEnd(this);
276 }
277
278 @Override
279 public void setSpan(Object what, int start, int end, int flags) {
280 super.setSpan(what, start, end, flags);
281 // Setting a span does not involve mutating the text value in the editing state. Here we create
282 // a non text update delta with any updated selection and composing regions.
283 mBatchTextEditingDeltas.add(
285 toString(),
289 getComposingEnd()));
290 }
291
292 @Override
293 public String toString() {
294 return mToStringCache != null ? mToStringCache : (mToStringCache = super.toString());
295 }
296}
static void v(@NonNull String tag, @NonNull String message)
Definition Log.java:40
static void e(@NonNull String tag, @NonNull String message)
Definition Log.java:84
static void w(@NonNull String tag, @NonNull String message)
Definition Log.java:76
ArrayList< TextEditingDelta > extractBatchTextEditingDeltas()
void removeEditingStateListener(EditingStateWatcher listener)
void setSpan(Object what, int start, int end, int flags)
void setEditingState(TextInputChannel.TextEditState newState)
SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend)
ListenableEditingState( @Nullable TextInputChannel.TextEditState initialState, @NonNull View view)
void addEditingStateListener(EditingStateWatcher listener)
void setComposingRange(int composingStart, int composingEnd)
FlutterSemanticsFlag flags
glong glong end
void didChangeEditingState(boolean textChanged, boolean selectionChanged, boolean composingRegionChanged)
size_t length
#define TAG()