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

Commit 033c373

Browse files
authored
Make DPAD movement consider grapheme clusters (#17420)
Moving the caret with the software Dpad now correctly considers complex characters.
1 parent 41c503c commit 033c373

File tree

3 files changed

+451
-4
lines changed

3 files changed

+451
-4
lines changed

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

+130
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,134 @@ public int getOffsetBefore(CharSequence text, int offset) {
186186

187187
return offset - deleteCharCount;
188188
}
189+
190+
/**
191+
* Gets the offset of the next character following the given offset, with consideration for
192+
* multi-byte characters.
193+
*
194+
* @see <a target="_new"
195+
* href="https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111">https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111</a>
196+
*/
197+
public int getOffsetAfter(CharSequence text, int offset) {
198+
final int len = text.length();
199+
200+
if (offset >= len - 1) {
201+
return len;
202+
}
203+
204+
int codePoint = Character.codePointAt(text, offset);
205+
int nextCharCount = Character.charCount(codePoint);
206+
int nextOffset = offset + nextCharCount;
207+
208+
if (nextOffset == 0) {
209+
return 0;
210+
}
211+
212+
// Line Feed
213+
if (codePoint == LINE_FEED) {
214+
codePoint = Character.codePointAt(text, nextOffset);
215+
if (codePoint == CARRIAGE_RETURN) {
216+
++nextCharCount;
217+
}
218+
return offset + nextCharCount;
219+
}
220+
221+
// Flags
222+
if (isRegionalIndicatorSymbol(codePoint)) {
223+
if (nextOffset >= len - 1
224+
|| !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) {
225+
return offset + nextCharCount;
226+
}
227+
// In this case there are at least two regional indicator symbols ahead of
228+
// offset. If those two regional indicator symbols are a pair that
229+
// represent a region together, the next offset should be after both of
230+
// them.
231+
int regionalIndicatorSymbolCount = 0;
232+
int regionOffset = offset;
233+
while (regionOffset > 0
234+
&& isRegionalIndicatorSymbol(Character.codePointBefore(text, offset))) {
235+
regionOffset -= Character.charCount(Character.codePointBefore(text, offset));
236+
regionalIndicatorSymbolCount++;
237+
}
238+
if (regionalIndicatorSymbolCount % 2 == 0) {
239+
nextCharCount += 2;
240+
}
241+
return offset + nextCharCount;
242+
}
243+
244+
// Keycaps
245+
if (isKeycapBase(codePoint)) {
246+
nextCharCount += Character.charCount(codePoint);
247+
}
248+
if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
249+
codePoint = Character.codePointBefore(text, nextOffset);
250+
nextOffset += Character.charCount(codePoint);
251+
if (nextOffset < len && isVariationSelector(codePoint)) {
252+
int tmpCodePoint = Character.codePointAt(text, nextOffset);
253+
if (isKeycapBase(tmpCodePoint)) {
254+
nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
255+
}
256+
} else if (isKeycapBase(codePoint)) {
257+
nextCharCount += Character.charCount(codePoint);
258+
}
259+
return offset + nextCharCount;
260+
}
261+
262+
if (isEmoji(codePoint)) {
263+
boolean isZwj = false;
264+
int lastSeenVariantSelectorCharCount = 0;
265+
do {
266+
if (isZwj) {
267+
nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1;
268+
isZwj = false;
269+
}
270+
lastSeenVariantSelectorCharCount = 0;
271+
if (isEmojiModifier(codePoint)) {
272+
break;
273+
}
274+
275+
if (nextOffset < len) {
276+
codePoint = Character.codePointAt(text, nextOffset);
277+
nextOffset += Character.charCount(codePoint);
278+
if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
279+
codePoint = Character.codePointBefore(text, nextOffset);
280+
nextOffset += Character.charCount(codePoint);
281+
if (nextOffset < len && isVariationSelector(codePoint)) {
282+
int tmpCodePoint = Character.codePointAt(text, nextOffset);
283+
if (isKeycapBase(tmpCodePoint)) {
284+
nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
285+
}
286+
} else if (isKeycapBase(codePoint)) {
287+
nextCharCount += Character.charCount(codePoint);
288+
}
289+
return offset + nextCharCount;
290+
}
291+
if (isEmojiModifier(codePoint)) {
292+
nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
293+
break;
294+
}
295+
if (isVariationSelector(codePoint)) {
296+
nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
297+
break;
298+
}
299+
if (codePoint == ZERO_WIDTH_JOINER) {
300+
isZwj = true;
301+
codePoint = Character.codePointAt(text, nextOffset);
302+
nextOffset += Character.charCount(codePoint);
303+
if (nextOffset < len && isVariationSelector(codePoint)) {
304+
codePoint = Character.codePointAt(text, nextOffset);
305+
lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
306+
nextOffset += Character.charCount(codePoint);
307+
}
308+
}
309+
}
310+
311+
if (nextOffset >= len) {
312+
break;
313+
}
314+
} while (isZwj && isEmoji(codePoint));
315+
}
316+
317+
return offset + nextCharCount;
318+
}
189319
}

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -343,21 +343,23 @@ public boolean sendKeyEvent(KeyEvent event) {
343343
int selStart = Selection.getSelectionStart(mEditable);
344344
int selEnd = Selection.getSelectionEnd(mEditable);
345345
if (selStart == selEnd && !event.isShiftPressed()) {
346-
int newSel = Math.max(selStart - 1, 0);
346+
int newSel = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selStart), 0);
347347
setSelection(newSel, newSel);
348348
} else {
349-
int newSelEnd = Math.max(selEnd - 1, 0);
349+
int newSelEnd = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selEnd), 0);
350350
setSelection(selStart, newSelEnd);
351351
}
352352
return true;
353353
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
354354
int selStart = Selection.getSelectionStart(mEditable);
355355
int selEnd = Selection.getSelectionEnd(mEditable);
356356
if (selStart == selEnd && !event.isShiftPressed()) {
357-
int newSel = Math.min(selStart + 1, mEditable.length());
357+
int newSel =
358+
Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart), mEditable.length());
358359
setSelection(newSel, newSel);
359360
} else {
360-
int newSelEnd = Math.min(selEnd + 1, mEditable.length());
361+
int newSelEnd =
362+
Math.min(flutterTextUtils.getOffsetAfter(mEditable, selEnd), mEditable.length());
361363
setSelection(selStart, newSelEnd);
362364
}
363365
return true;

0 commit comments

Comments
 (0)