diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 3bcff03e5f4be4..55d5cea6ae612d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -601,10 +601,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 shouldKeepCompositeSpan = length() == spannableStringBuilder.length(); + getText().replace(0, length(), spannableStringBuilder); + + if (shouldKeepCompositeSpan) { + attachCompositeSpansToTextFrom(spannableStringBuilder); + } } mDisableTextDiffing = false; @@ -619,10 +622,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 + * 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, boolean skipAddSpansForMeasurements) { @@ -632,6 +638,8 @@ private void manageSpans( 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) { @@ -646,6 +654,12 @@ private void manageSpans( 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); @@ -683,6 +697,33 @@ private void stripAtributeEquivalentSpans(SpannableStringBuilder sb) { } } + /** + * 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 attachCompositeSpansToTextFrom(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 void unstripAttributeEquivalentSpans( SpannableStringBuilder workingText, Spannable originalText) { // We must add spans back for Fabric to be able to measure, at lower precedence than any