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 4/N: ReactForegroundColorSpan #36545

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
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 @@ -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;
Expand Down Expand Up @@ -50,6 +51,8 @@
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.ReactForegroundColorSpan;
import com.facebook.react.views.text.ReactSpan;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTypefaceUtils;
Expand Down Expand Up @@ -585,9 +588,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,49 +663,81 @@ 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;
}
}
stripSpansOfKind(
sb,
ReactBackgroundColorSpan.class,
new SpanPredicate<ReactBackgroundColorSpan>() {
@Override
public boolean test(ReactBackgroundColorSpan span) {
return span.getBackgroundColor() == mReactBackgroundManager.getBackgroundColor();
}
});

sb.removeSpan(span);
}
stripSpansOfKind(
sb,
ReactForegroundColorSpan.class,
new SpanPredicate<ReactForegroundColorSpan>() {
@Override
public boolean test(ReactForegroundColorSpan span) {
return span.getForegroundColor() == getCurrentTextColor();
}
});
}

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);
private <T> void stripSpansOfKind(
SpannableStringBuilder sb, Class<T> clazz, SpanPredicate<T> shouldStrip) {
T[] spans = sb.getSpans(0, sb.length(), clazz);

for (Object span : workingText.getSpans(0, workingText.length(), Object.class)) {
workingText.removeSpan(span);
for (T span : spans) {
if (shouldStrip.test(span)) {
sb.removeSpan(span);
}
}
}

workingText.setSpan(
new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()),
0,
workingText.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
/**
* 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 : originalText.getSpans(0, originalText.length(), Object.class)) {
workingText.setSpan(
span,
originalText.getSpanStart(span),
originalText.getSpanEnd(span),
originalText.getSpanFlags(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<Object> spans = new ArrayList<>();
spans.add(new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()));
spans.add(new ReactForegroundColorSpan(getCurrentTextColor()));

int backgroundColor = mReactBackgroundManager.getBackgroundColor();
if (backgroundColor != Color.TRANSPARENT) {
spans.add(new ReactBackgroundColorSpan(backgroundColor));
}

for (Object span : spans) {
workingText.setSpan(span, 0, workingText.length(), spanFlags);
}
}

Expand Down Expand Up @@ -1132,8 +1165,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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down