From 3d544c1fad36531f4ea1af5de17b137aa0bfa0d4 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 23 Mar 2023 23:37:36 -0700 Subject: [PATCH] Minimize EditText Spans 2/9: Make stripAttributeEquivalentSpans generic (#36546) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36546 This is part of a series of changes to minimize the number of spans committed to EditText, as a mitigation for platform issues on Samsung devices. See this [GitHub thread]( https://github.com/facebook/react-native/issues/35936#issuecomment-1411437789) for greater context on the platform behavior. This change generalizes `stripAttributeEquivalentSpans()` to allow plugging in different spans. Changelog: [Internal] Differential Revision: https://www.internalfb.com/diff/D44240781?entry_point=27 fbshipit-source-id: 738af283ddc9eb593a6cada069b505a91fd30909 --- .../react/views/textinput/ReactEditText.java | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) 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 837f79702f12ce..b9c4461395bcf4 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 @@ -585,9 +585,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,28 +660,44 @@ 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); + } + + /** + * 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(); + } + }); + } - outerLoop: - for (ReactAbsoluteSizeSpan span : spans) { - ReactAbsoluteSizeSpan[] overlappingSpans = - sb.getSpans(sb.getSpanStart(span), sb.getSpanEnd(span), ReactAbsoluteSizeSpan.class); + private void stripSpansOfKind( + SpannableStringBuilder sb, Class clazz, SpanPredicate shouldStrip) { + T[] spans = sb.getSpans(0, sb.length(), clazz); - for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) { - if (span.getSize() != effectiveFontSize) { - continue outerLoop; - } + for (T span : spans) { + if (shouldStrip.test(span)) { + sb.removeSpan(span); } - - sb.removeSpan(span); } } - private void unstripAttributeEquivalentSpans(SpannableStringBuilder workingText) { + /** + * 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; // Set all bits for SPAN_PRIORITY so that this span has the highest possible priority @@ -1122,7 +1136,7 @@ private void updateCachedSpannable(boolean resetStyles) { // - android.app.Activity.dispatchKeyEvent (Activity.java:3447) try { sb.append(currentText.subSequence(0, currentText.length())); - unstripAttributeEquivalentSpans(sb); + restoreStyleEquivalentSpans(sb); } catch (IndexOutOfBoundsException e) { ReactSoftExceptionLogger.logSoftException(TAG, e); }