Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimize EditText Spans 2/N: Make stripAttributeEquivalentSpans generic #36546

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -662,50 +660,55 @@ 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<T> and lambdas once Java 8 builds in OSS
interface SpanPredicate<T> {
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<ReactAbsoluteSizeSpan>() {
@Override
public boolean test(ReactAbsoluteSizeSpan span) {
return span.getSize() == mTextAttributes.getEffectiveFontSize();
}
});
}

for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) {
if (span.getSize() != effectiveFontSize) {
continue outerLoop;
}
}
private <T> void stripSpansOfKind(
SpannableStringBuilder sb, Class<T> clazz, SpanPredicate<T> 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;

workingText.setSpan(
new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()),
0,
workingText.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);

for (Object span : originalText.getSpans(0, originalText.length(), Object.class)) {
workingText.setSpan(
span,
originalText.getSpanStart(span),
originalText.getSpanEnd(span),
originalText.getSpanFlags(span));
}
spanFlags);
}

private static boolean sameTextForSpan(
Expand Down Expand Up @@ -1132,8 +1135,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);
}
Expand Down