diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 448e94885dc21a..55c92a7df207f2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -632,10 +632,13 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { if (reactTextUpdate.getText().length() == 0) { setText(null); } else { - // When we update text, we trigger onChangeText code that will - // try to update state if the wrapper is available. Temporarily disable - // to prevent an infinite loop. + boolean shouldRestoreComposingSpans = length() == spannableStringBuilder.length(); + getText().replace(0, length(), spannableStringBuilder); + + if (shouldRestoreComposingSpans) { + restoreComposingSpansToTextFrom(spannableStringBuilder); + } } mDisableTextDiffing = false; @@ -650,10 +653,13 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { } /** - * Remove and/or add {@link Spanned.SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist - * as long as the text they cover is the same. All other spans will remain the same, since they - * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes - * them. + * Remove and/or add {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist + * as long as the text they cover is the same unless they are {@link Spanned#SPAN_COMPOSING}. All + * other spans will remain the same, since they will adapt to the new text, hence why {@link + * SpannableStringBuilder#replace} never removes them. Keep copy of {@link Spanned#SPAN_COMPOSING} + * Spans in {@param spannableStringBuilder}, because they are important for keyboard suggestions. + * Without keeping these Spans, suggestions default to be put after the current selection + * position, possibly resulting in letter duplication. */ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { Object[] spans = getText().getSpans(0, length(), Object.class); @@ -662,6 +668,7 @@ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { int spanFlags = getText().getSpanFlags(span); boolean isExclusiveExclusive = (spanFlags & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + boolean isComposing = (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; // Remove all styling spans we might have previously set if (span instanceof ReactSpan) { @@ -676,6 +683,12 @@ private void manageSpans(SpannableStringBuilder spannableStringBuilder) { final int spanStart = getText().getSpanStart(span); final int spanEnd = getText().getSpanEnd(span); + // We keep a copy of Composing spans + if (isComposing) { + spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); + continue; + } + // Make sure the span is removed from existing text, otherwise the spans we set will be // ignored or it will cover text that has changed. getText().removeSpan(span); @@ -803,6 +816,34 @@ private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) { } } + /** + * Attaches the {@link Spanned#SPAN_COMPOSING} from {@param spannableStringBuilder} to {@link + * ReactEditText#getText} + * + *

See {@link ReactEditText#manageSpans} for more details. Also + * https://github.com/facebook/react-native/issues/11068 + */ + private void restoreComposingSpansToTextFrom(SpannableStringBuilder spannableStringBuilder) { + Editable text = getText(); + if (text == null) { + return; + } + Object[] spans = spannableStringBuilder.getSpans(0, length(), Object.class); + for (Object span : spans) { + int spanFlags = spannableStringBuilder.getSpanFlags(span); + boolean isComposing = (spanFlags & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING; + + if (!isComposing) { + continue; + } + + final int spanStart = spannableStringBuilder.getSpanStart(span); + final int spanEnd = spannableStringBuilder.getSpanEnd(span); + + text.setSpan(span, spanStart, spanEnd, spanFlags); + } + } + private static boolean sameTextForSpan( final Editable oldText, final SpannableStringBuilder newText,