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 07c95b243db62e..aba83f2b289e8a 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 @@ -11,6 +11,7 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -50,6 +51,7 @@ import com.facebook.react.views.text.CustomLineHeightSpan; import com.facebook.react.views.text.CustomStyleSpan; import com.facebook.react.views.text.ReactAbsoluteSizeSpan; +import com.facebook.react.views.text.ReactBackgroundColorSpan; import com.facebook.react.views.text.ReactSpan; import com.facebook.react.views.text.ReactTextUpdate; import com.facebook.react.views.text.ReactTypefaceUtils; @@ -585,9 +587,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { new SpannableStringBuilder(reactTextUpdate.getText()); manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments); - - // Mitigation for https://github.com/facebook/react-native/issues/35936 (S318090) - stripAttributeEquivalentSpans(spannableStringBuilder); + stripStyleEquivalentSpans(spannableStringBuilder); mContainsImages = reactTextUpdate.containsImages(); @@ -662,49 +662,70 @@ private void manageSpans( } } - private void stripAttributeEquivalentSpans(SpannableStringBuilder sb) { - // We have already set a font size on the EditText itself. We can safely remove sizing spans - // which are the same as the set font size, and not otherwise overlapped. - final int effectiveFontSize = mTextAttributes.getEffectiveFontSize(); - ReactAbsoluteSizeSpan[] spans = sb.getSpans(0, sb.length(), ReactAbsoluteSizeSpan.class); + // TODO: Replace with Predicate and lambdas once Java 8 builds in OSS + interface SpanPredicate { + boolean test(T span); + } - outerLoop: - for (ReactAbsoluteSizeSpan span : spans) { - ReactAbsoluteSizeSpan[] overlappingSpans = - sb.getSpans(sb.getSpanStart(span), sb.getSpanEnd(span), ReactAbsoluteSizeSpan.class); + /** + * Remove spans from the SpannableStringBuilder which can be represented by TextAppearance + * attributes on the underlying EditText. This works around instability on Samsung devices with + * the presence of spans https://github.com/facebook/react-native/issues/35936 (S318090) + */ + private void stripStyleEquivalentSpans(SpannableStringBuilder sb) { + stripSpansOfKind( + sb, + ReactAbsoluteSizeSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactAbsoluteSizeSpan span) { + return span.getSize() == mTextAttributes.getEffectiveFontSize(); + } + }); - for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) { - if (span.getSize() != effectiveFontSize) { - continue outerLoop; - } - } + stripSpansOfKind( + sb, + ReactBackgroundColorSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactBackgroundColorSpan span) { + return span.getBackgroundColor() == mReactBackgroundManager.getBackgroundColor(); + } + }); + } + + private void stripSpansOfKind( + SpannableStringBuilder sb, Class clazz, SpanPredicate shouldStrip) { + T[] spans = sb.getSpans(0, sb.length(), clazz); - sb.removeSpan(span); + for (T span : spans) { + if (shouldStrip.test(span)) { + sb.removeSpan(span); + } } } - private void unstripAttributeEquivalentSpans( - SpannableStringBuilder workingText, Spannable originalText) { - // We must add spans back for Fabric to be able to measure, at lower precedence than any - // existing spans. Remove all spans, add the attributes, then re-add the spans over - workingText.append(originalText); + /** + * Copy back styles represented as attributes to the underlying span, for later measurement + * outside the ReactEditText. + */ + private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) { + int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE; - for (Object span : workingText.getSpans(0, workingText.length(), Object.class)) { - workingText.removeSpan(span); - } + // Set all bits for SPAN_PRIORITY so that this span has the highest possible priority + // (least precedence). This ensures the span is behind any overlapping spans. + spanFlags |= Spannable.SPAN_PRIORITY; + + List spans = new ArrayList<>(); + spans.add(new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize())); - workingText.setSpan( - new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()), - 0, - workingText.length(), - Spanned.SPAN_INCLUSIVE_INCLUSIVE); + int backgroundColor = mReactBackgroundManager.getBackgroundColor(); + if (backgroundColor != Color.TRANSPARENT) { + spans.add(new ReactBackgroundColorSpan(backgroundColor)); + } - for (Object span : originalText.getSpans(0, originalText.length(), Object.class)) { - workingText.setSpan( - span, - originalText.getSpanStart(span), - originalText.getSpanEnd(span), - originalText.getSpanFlags(span)); + for (Object span : spans) { + workingText.setSpan(span, 0, workingText.length(), spanFlags); } } @@ -1132,8 +1153,8 @@ private void updateCachedSpannable(boolean resetStyles) { // ... // - android.app.Activity.dispatchKeyEvent (Activity.java:3447) try { - Spannable text = (Spannable) currentText.subSequence(0, currentText.length()); - unstripAttributeEquivalentSpans(sb, text); + sb.append(currentText.subSequence(0, currentText.length())); + restoreStyleEquivalentSpans(sb); } catch (IndexOutOfBoundsException e) { ReactSoftExceptionLogger.logSoftException(TAG, e); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java index 4a5fce52c3a9e5..9b479fa42214be 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java @@ -19,6 +19,7 @@ public class ReactViewBackgroundManager { private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private View mView; + private int mColor = Color.TRANSPARENT; public ReactViewBackgroundManager(View view) { this.mView = view; @@ -56,6 +57,10 @@ public void setBackgroundColor(int color) { } } + public int getBackgroundColor() { + return mColor; + } + public void setBorderWidth(int position, float width) { getOrCreateReactViewBackground().setBorderWidth(position, width); }