未验证 提交 033c3737 编写于 作者: J Justin McCandless 提交者: GitHub

Make DPAD movement consider grapheme clusters (#17420)

Moving the caret with the software Dpad now correctly considers complex characters.
上级 41c503cc
......@@ -186,4 +186,134 @@ class FlutterTextUtils {
return offset - deleteCharCount;
}
/**
* Gets the offset of the next character following the given offset, with consideration for
* multi-byte characters.
*
* @see <a target="_new"
* 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>
*/
public int getOffsetAfter(CharSequence text, int offset) {
final int len = text.length();
if (offset >= len - 1) {
return len;
}
int codePoint = Character.codePointAt(text, offset);
int nextCharCount = Character.charCount(codePoint);
int nextOffset = offset + nextCharCount;
if (nextOffset == 0) {
return 0;
}
// Line Feed
if (codePoint == LINE_FEED) {
codePoint = Character.codePointAt(text, nextOffset);
if (codePoint == CARRIAGE_RETURN) {
++nextCharCount;
}
return offset + nextCharCount;
}
// Flags
if (isRegionalIndicatorSymbol(codePoint)) {
if (nextOffset >= len - 1
|| !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) {
return offset + nextCharCount;
}
// In this case there are at least two regional indicator symbols ahead of
// offset. If those two regional indicator symbols are a pair that
// represent a region together, the next offset should be after both of
// them.
int regionalIndicatorSymbolCount = 0;
int regionOffset = offset;
while (regionOffset > 0
&& isRegionalIndicatorSymbol(Character.codePointBefore(text, offset))) {
regionOffset -= Character.charCount(Character.codePointBefore(text, offset));
regionalIndicatorSymbolCount++;
}
if (regionalIndicatorSymbolCount % 2 == 0) {
nextCharCount += 2;
}
return offset + nextCharCount;
}
// Keycaps
if (isKeycapBase(codePoint)) {
nextCharCount += Character.charCount(codePoint);
}
if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
codePoint = Character.codePointBefore(text, nextOffset);
nextOffset += Character.charCount(codePoint);
if (nextOffset < len && isVariationSelector(codePoint)) {
int tmpCodePoint = Character.codePointAt(text, nextOffset);
if (isKeycapBase(tmpCodePoint)) {
nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
}
} else if (isKeycapBase(codePoint)) {
nextCharCount += Character.charCount(codePoint);
}
return offset + nextCharCount;
}
if (isEmoji(codePoint)) {
boolean isZwj = false;
int lastSeenVariantSelectorCharCount = 0;
do {
if (isZwj) {
nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1;
isZwj = false;
}
lastSeenVariantSelectorCharCount = 0;
if (isEmojiModifier(codePoint)) {
break;
}
if (nextOffset < len) {
codePoint = Character.codePointAt(text, nextOffset);
nextOffset += Character.charCount(codePoint);
if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
codePoint = Character.codePointBefore(text, nextOffset);
nextOffset += Character.charCount(codePoint);
if (nextOffset < len && isVariationSelector(codePoint)) {
int tmpCodePoint = Character.codePointAt(text, nextOffset);
if (isKeycapBase(tmpCodePoint)) {
nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
}
} else if (isKeycapBase(codePoint)) {
nextCharCount += Character.charCount(codePoint);
}
return offset + nextCharCount;
}
if (isEmojiModifier(codePoint)) {
nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
break;
}
if (isVariationSelector(codePoint)) {
nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
break;
}
if (codePoint == ZERO_WIDTH_JOINER) {
isZwj = true;
codePoint = Character.codePointAt(text, nextOffset);
nextOffset += Character.charCount(codePoint);
if (nextOffset < len && isVariationSelector(codePoint)) {
codePoint = Character.codePointAt(text, nextOffset);
lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
nextOffset += Character.charCount(codePoint);
}
}
}
if (nextOffset >= len) {
break;
}
} while (isZwj && isEmoji(codePoint));
}
return offset + nextCharCount;
}
}
......@@ -343,10 +343,10 @@ class InputConnectionAdaptor extends BaseInputConnection {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
int newSel = Math.max(selStart - 1, 0);
int newSel = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selStart), 0);
setSelection(newSel, newSel);
} else {
int newSelEnd = Math.max(selEnd - 1, 0);
int newSelEnd = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selEnd), 0);
setSelection(selStart, newSelEnd);
}
return true;
......@@ -354,10 +354,12 @@ class InputConnectionAdaptor extends BaseInputConnection {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
int newSel = Math.min(selStart + 1, mEditable.length());
int newSel =
Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart), mEditable.length());
setSelection(newSel, newSel);
} else {
int newSelEnd = Math.min(selEnd + 1, mEditable.length());
int newSelEnd =
Math.min(flutterTextUtils.getOffsetAfter(mEditable, selEnd), mEditable.length());
setSelection(selStart, newSelEnd);
}
return true;
......
......@@ -154,6 +154,148 @@ public class InputConnectionAdaptorTest {
assertEquals(selStart - 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_leftKeyMovesCaretLeftComplexEmoji() {
int selStart = 75;
Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
boolean didConsume;
// Normal Character
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 74);
// Non-Spacing Mark
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 73);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 72);
// Keycap
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 69);
// Keycap with invalid base
adaptor.setSelection(68, 68);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
adaptor.setSelection(67, 67);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
// Zero Width Joiner
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 55);
// Zero Width Joiner with invalid base
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 53);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 52);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 51);
// ----- Start Emoji Tag Sequence with invalid base testing ----
// Delete base tag
adaptor.setSelection(39, 39);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 37);
// Delete the sequence
adaptor.setSelection(49, 49);
for (int i = 0; i < 6; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
}
assertEquals(Selection.getSelectionStart(editable), 37);
// ----- End Emoji Tag Sequence with invalid base testing ----
// Emoji Tag Sequence
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 23);
// Variation Selector with invalid base
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
// Variation Selector
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 19);
// Emoji Modifier
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier with invalid base
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
// Line Feed
adaptor.setSelection(12, 12);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 11);
// Carriage Return
adaptor.setSelection(12, 12);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 11);
// Carriage Return and Line Feed
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 9);
// Regional Indicator Symbol odd
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 7);
// Regional Indicator Symbol even
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 3);
// Simple Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 1);
// First CodePoint
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 0);
}
@Test
public void testSendKeyEvent_leftKeyExtendsSelectionLeft() {
int selStart = 5;
......@@ -199,6 +341,179 @@ public class InputConnectionAdaptorTest {
assertEquals(selStart + 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_rightKeyMovesCaretRightComplexRegion() {
int selStart = 0;
// Seven region indicator characters. The first six should be considered as
// three region indicators, and the final seventh character should be
// considered to be on its own because it has no partner.
String SAMPLE_REGION_TEXT = "🇷🇷🇷🇷🇷🇷🇷";
Editable editable = sampleEditable(selStart, selStart, SAMPLE_REGION_TEXT);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume;
// The cursor moves over two region indicators at a time.
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 4);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 8);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 12);
// When there is only one region indicator left with no pair, the cursor
// moves over that single region indicator.
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 14);
// If the cursor is placed in the middle of a region indicator pair, it
// moves over only the second half of the pair.
adaptor.setSelection(6, 6);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 8);
}
@Test
public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() {
int selStart = 0;
Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume;
// First CodePoint
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 1);
// Simple Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 3);
// Regional Indicator Symbol even
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 7);
// Regional Indicator Symbol odd
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 9);
// Carriage Return
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 10);
// Line Feed and Carriage Return
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 12);
// Line Feed
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
// Modified Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier with invalid base
adaptor.setSelection(18, 18);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 19);
// Variation Selector
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
// Variation Selector with invalid base
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 23);
// Emoji Tag Sequence
for (int i = 0; i < 7; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 25 + 2 * i);
}
assertEquals(Selection.getSelectionStart(editable), 37);
// ----- Start Emoji Tag Sequence with invalid base testing ----
// Pass the sequence
adaptor.setSelection(39, 39);
for (int i = 0; i < 6; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 41 + 2 * i);
}
assertEquals(Selection.getSelectionStart(editable), 51);
// ----- End Emoji Tag Sequence with invalid base testing ----
// Zero Width Joiner with invalid base
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 52);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 53);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 55);
// Zero Width Joiner
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
// Keycap with invalid base
adaptor.setSelection(67, 67);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 68);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 69);
// Keycap
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 72);
// Non-Spacing Mark
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 73);
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 74);
// Normal Character
didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 75);
}
@Test
public void testSendKeyEvent_rightKeyExtendsSelectionRight() {
int selStart = 5;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册