Skip to content

Commit 5834cea

Browse files
authoredApr 25, 2023
Merge pull request #37000 from facebook/kelset/attempt-3-backporting-textinput-fixes
2 parents f4f3aa3 + deb7cda commit 5834cea

File tree

5 files changed

+218
-18
lines changed

5 files changed

+218
-18
lines changed
 

‎ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public void updateMeasureState(TextPaint paint) {
3737
apply(paint);
3838
}
3939

40+
public float getSpacing() {
41+
return mLetterSpacing;
42+
}
43+
4044
private void apply(TextPaint paint) {
4145
if (!Float.isNaN(mLetterSpacing)) {
4246
paint.setLetterSpacing(mLetterSpacing);

‎ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public int getWeight() {
7171
return mFontFamily;
7272
}
7373

74+
public @Nullable String getFontFeatureSettings() {
75+
return mFeatureSettings;
76+
}
77+
7478
private static void apply(
7579
Paint paint,
7680
int style,

‎ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

+184-18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import static com.facebook.react.views.text.TextAttributeProps.UNSET;
1212

1313
import android.content.Context;
14+
import android.graphics.Color;
15+
import android.graphics.Paint;
1416
import android.graphics.Rect;
1517
import android.graphics.Typeface;
1618
import android.graphics.drawable.Drawable;
@@ -50,15 +52,20 @@
5052
import com.facebook.react.views.text.CustomLineHeightSpan;
5153
import com.facebook.react.views.text.CustomStyleSpan;
5254
import com.facebook.react.views.text.ReactAbsoluteSizeSpan;
55+
import com.facebook.react.views.text.ReactBackgroundColorSpan;
56+
import com.facebook.react.views.text.ReactForegroundColorSpan;
5357
import com.facebook.react.views.text.ReactSpan;
58+
import com.facebook.react.views.text.ReactStrikethroughSpan;
5459
import com.facebook.react.views.text.ReactTextUpdate;
5560
import com.facebook.react.views.text.ReactTypefaceUtils;
61+
import com.facebook.react.views.text.ReactUnderlineSpan;
5662
import com.facebook.react.views.text.TextAttributes;
5763
import com.facebook.react.views.text.TextInlineImageSpan;
5864
import com.facebook.react.views.text.TextLayoutManager;
5965
import com.facebook.react.views.view.ReactViewBackgroundManager;
6066
import java.util.ArrayList;
6167
import java.util.List;
68+
import java.util.Objects;
6269

6370
/**
6471
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@@ -477,6 +484,14 @@ public void setFontStyle(String fontStyleString) {
477484
}
478485
}
479486

487+
@Override
488+
public void setFontFeatureSettings(String fontFeatureSettings) {
489+
if (!Objects.equals(fontFeatureSettings, getFontFeatureSettings())) {
490+
super.setFontFeatureSettings(fontFeatureSettings);
491+
mTypefaceDirty = true;
492+
}
493+
}
494+
480495
public void maybeUpdateTypeface() {
481496
if (!mTypefaceDirty) {
482497
return;
@@ -488,6 +503,17 @@ public void maybeUpdateTypeface() {
488503
ReactTypefaceUtils.applyStyles(
489504
getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets());
490505
setTypeface(newTypeface);
506+
507+
// Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
508+
// nonstandard
509+
if (mFontStyle != UNSET
510+
|| mFontWeight != UNSET
511+
|| mFontFamily != null
512+
|| getFontFeatureSettings() != null) {
513+
setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
514+
} else {
515+
setPaintFlags(getPaintFlags() & (~Paint.SUBPIXEL_TEXT_FLAG));
516+
}
491517
}
492518

493519
// VisibleForTesting from {@link TextInputEventsTestCase}.
@@ -550,9 +576,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
550576
new SpannableStringBuilder(reactTextUpdate.getText());
551577

552578
manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments);
553-
554-
// Mitigation for https://github.com/facebook/react-native/issues/35936 (S318090)
555-
stripAbsoluteSizeSpans(spannableStringBuilder);
579+
stripStyleEquivalentSpans(spannableStringBuilder);
556580

557581
mContainsImages = reactTextUpdate.containsImages();
558582

@@ -627,24 +651,163 @@ private void manageSpans(
627651
}
628652
}
629653

630-
private void stripAbsoluteSizeSpans(SpannableStringBuilder sb) {
631-
// We have already set a font size on the EditText itself. We can safely remove sizing spans
632-
// which are the same as the set font size, and not otherwise overlapped.
633-
final int effectiveFontSize = mTextAttributes.getEffectiveFontSize();
634-
ReactAbsoluteSizeSpan[] spans = sb.getSpans(0, sb.length(), ReactAbsoluteSizeSpan.class);
654+
// TODO: Replace with Predicate<T> and lambdas once Java 8 builds in OSS
655+
interface SpanPredicate<T> {
656+
boolean test(T span);
657+
}
658+
659+
/**
660+
* Remove spans from the SpannableStringBuilder which can be represented by TextAppearance
661+
* attributes on the underlying EditText. This works around instability on Samsung devices with
662+
* the presence of spans https://github.com/facebook/react-native/issues/35936 (S318090)
663+
*/
664+
private void stripStyleEquivalentSpans(SpannableStringBuilder sb) {
665+
stripSpansOfKind(
666+
sb,
667+
ReactAbsoluteSizeSpan.class,
668+
new SpanPredicate<ReactAbsoluteSizeSpan>() {
669+
@Override
670+
public boolean test(ReactAbsoluteSizeSpan span) {
671+
return span.getSize() == mTextAttributes.getEffectiveFontSize();
672+
}
673+
});
674+
675+
stripSpansOfKind(
676+
sb,
677+
ReactBackgroundColorSpan.class,
678+
new SpanPredicate<ReactBackgroundColorSpan>() {
679+
@Override
680+
public boolean test(ReactBackgroundColorSpan span) {
681+
return span.getBackgroundColor() == mReactBackgroundManager.getBackgroundColor();
682+
}
683+
});
635684

636-
outerLoop:
637-
for (ReactAbsoluteSizeSpan span : spans) {
638-
ReactAbsoluteSizeSpan[] overlappingSpans =
639-
sb.getSpans(sb.getSpanStart(span), sb.getSpanEnd(span), ReactAbsoluteSizeSpan.class);
685+
stripSpansOfKind(
686+
sb,
687+
ReactForegroundColorSpan.class,
688+
new SpanPredicate<ReactForegroundColorSpan>() {
689+
@Override
690+
public boolean test(ReactForegroundColorSpan span) {
691+
return span.getForegroundColor() == getCurrentTextColor();
692+
}
693+
});
640694

641-
for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) {
642-
if (span.getSize() != effectiveFontSize) {
643-
continue outerLoop;
644-
}
695+
stripSpansOfKind(
696+
sb,
697+
ReactStrikethroughSpan.class,
698+
new SpanPredicate<ReactStrikethroughSpan>() {
699+
@Override
700+
public boolean test(ReactStrikethroughSpan span) {
701+
return (getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0;
702+
}
703+
});
704+
705+
stripSpansOfKind(
706+
sb,
707+
ReactUnderlineSpan.class,
708+
new SpanPredicate<ReactUnderlineSpan>() {
709+
@Override
710+
public boolean test(ReactUnderlineSpan span) {
711+
return (getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0;
712+
}
713+
});
714+
715+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
716+
stripSpansOfKind(
717+
sb,
718+
CustomLetterSpacingSpan.class,
719+
new SpanPredicate<CustomLetterSpacingSpan>() {
720+
@Override
721+
public boolean test(CustomLetterSpacingSpan span) {
722+
return span.getSpacing() == mTextAttributes.getEffectiveLetterSpacing();
723+
}
724+
});
725+
}
726+
727+
stripSpansOfKind(
728+
sb,
729+
CustomStyleSpan.class,
730+
new SpanPredicate<CustomStyleSpan>() {
731+
@Override
732+
public boolean test(CustomStyleSpan span) {
733+
return span.getStyle() == mFontStyle
734+
&& Objects.equals(span.getFontFamily(), mFontFamily)
735+
&& span.getWeight() == mFontWeight
736+
&& Objects.equals(span.getFontFeatureSettings(), getFontFeatureSettings());
737+
}
738+
});
739+
}
740+
741+
private <T> void stripSpansOfKind(
742+
SpannableStringBuilder sb, Class<T> clazz, SpanPredicate<T> shouldStrip) {
743+
T[] spans = sb.getSpans(0, sb.length(), clazz);
744+
745+
for (T span : spans) {
746+
if (shouldStrip.test(span)) {
747+
sb.removeSpan(span);
748+
}
749+
}
750+
}
751+
752+
/**
753+
* Copy back styles represented as attributes to the underlying span, for later measurement
754+
* outside the ReactEditText.
755+
*/
756+
private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
757+
int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
758+
759+
// Set all bits for SPAN_PRIORITY so that this span has the highest possible priority
760+
// (least precedence). This ensures the span is behind any overlapping spans.
761+
spanFlags |= Spannable.SPAN_PRIORITY;
762+
763+
workingText.setSpan(
764+
new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()),
765+
0,
766+
workingText.length(),
767+
spanFlags);
768+
769+
workingText.setSpan(
770+
new ReactForegroundColorSpan(getCurrentTextColor()), 0, workingText.length(), spanFlags);
771+
772+
int backgroundColor = mReactBackgroundManager.getBackgroundColor();
773+
if (backgroundColor != Color.TRANSPARENT) {
774+
workingText.setSpan(
775+
new ReactBackgroundColorSpan(backgroundColor), 0, workingText.length(), spanFlags);
776+
}
777+
778+
if ((getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
779+
workingText.setSpan(new ReactStrikethroughSpan(), 0, workingText.length(), spanFlags);
780+
}
781+
782+
if ((getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0) {
783+
workingText.setSpan(new ReactUnderlineSpan(), 0, workingText.length(), spanFlags);
784+
}
785+
786+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
787+
float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
788+
if (!Float.isNaN(effectiveLetterSpacing)) {
789+
workingText.setSpan(
790+
new CustomLetterSpacingSpan(effectiveLetterSpacing),
791+
0,
792+
workingText.length(),
793+
spanFlags);
645794
}
795+
}
646796

647-
sb.removeSpan(span);
797+
if (mFontStyle != UNSET
798+
|| mFontWeight != UNSET
799+
|| mFontFamily != null
800+
|| getFontFeatureSettings() != null) {
801+
workingText.setSpan(
802+
new CustomStyleSpan(
803+
mFontStyle,
804+
mFontWeight,
805+
getFontFeatureSettings(),
806+
mFontFamily,
807+
getContext().getAssets()),
808+
0,
809+
workingText.length(),
810+
spanFlags);
648811
}
649812
}
650813

@@ -994,7 +1157,9 @@ protected void applyTextAttributes() {
9941157

9951158
float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
9961159
if (!Float.isNaN(effectiveLetterSpacing)) {
997-
setLetterSpacing(effectiveLetterSpacing);
1160+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1161+
setLetterSpacing(effectiveLetterSpacing);
1162+
}
9981163
}
9991164
}
10001165

@@ -1067,6 +1232,7 @@ private void updateCachedSpannable(boolean resetStyles) {
10671232
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
10681233
try {
10691234
sb.append(currentText.subSequence(0, currentText.length()));
1235+
restoreStyleEquivalentSpans(sb);
10701236
} catch (IndexOutOfBoundsException e) {
10711237
ReactSoftExceptionLogger.logSoftException(TAG, e);
10721238
}

‎ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java

+21
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import android.content.res.ColorStateList;
1414
import android.graphics.BlendMode;
1515
import android.graphics.BlendModeColorFilter;
16+
import android.graphics.Paint;
1617
import android.graphics.PorterDuff;
1718
import android.graphics.drawable.Drawable;
1819
import android.os.Build;
@@ -67,6 +68,7 @@
6768
import com.facebook.react.views.text.ReactBaseTextShadowNode;
6869
import com.facebook.react.views.text.ReactTextUpdate;
6970
import com.facebook.react.views.text.ReactTextViewManagerCallback;
71+
import com.facebook.react.views.text.ReactTypefaceUtils;
7072
import com.facebook.react.views.text.TextAttributeProps;
7173
import com.facebook.react.views.text.TextInlineImageSpan;
7274
import com.facebook.react.views.text.TextLayoutManager;
@@ -397,6 +399,11 @@ public void setFontStyle(ReactEditText view, @Nullable String fontStyle) {
397399
view.setFontStyle(fontStyle);
398400
}
399401

402+
@ReactProp(name = ViewProps.FONT_VARIANT)
403+
public void setFontVariant(ReactEditText view, @Nullable ReadableArray fontVariant) {
404+
view.setFontFeatureSettings(ReactTypefaceUtils.parseFontVariant(fontVariant));
405+
}
406+
400407
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
401408
public void setIncludeFontPadding(ReactEditText view, boolean includepad) {
402409
view.setIncludeFontPadding(includepad);
@@ -913,6 +920,20 @@ public void setAutoFocus(ReactEditText view, boolean autoFocus) {
913920
view.setAutoFocus(autoFocus);
914921
}
915922

923+
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
924+
public void setTextDecorationLine(ReactEditText view, @Nullable String textDecorationLineString) {
925+
view.setPaintFlags(
926+
view.getPaintFlags() & ~(Paint.STRIKE_THRU_TEXT_FLAG | Paint.UNDERLINE_TEXT_FLAG));
927+
928+
for (String token : textDecorationLineString.split(" ")) {
929+
if (token.equals("underline")) {
930+
view.setPaintFlags(view.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
931+
} else if (token.equals("line-through")) {
932+
view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
933+
}
934+
}
935+
}
936+
916937
@ReactPropGroup(
917938
names = {
918939
ViewProps.BORDER_WIDTH,

‎ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ReactViewBackgroundManager {
1919

2020
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
2121
private View mView;
22+
private int mColor = Color.TRANSPARENT;
2223

2324
public ReactViewBackgroundManager(View view) {
2425
this.mView = view;
@@ -50,6 +51,10 @@ public void setBackgroundColor(int color) {
5051
}
5152
}
5253

54+
public int getBackgroundColor() {
55+
return mColor;
56+
}
57+
5358
public void setBorderWidth(int position, float width) {
5459
getOrCreateReactViewBackground().setBorderWidth(position, width);
5560
}

0 commit comments

Comments
 (0)
Please sign in to comment.