Skip to content

Commit 558bc33

Browse files
authored
Merge pull request #37084 from facebook/kelset/068-backport-samsung-fixes
2 parents 91a3a54 + e89f919 commit 558bc33

File tree

6 files changed

+224
-24
lines changed

6 files changed

+224
-24
lines changed

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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 184 additions & 18 deletions
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
@@ -476,6 +483,14 @@ public void setFontStyle(String fontStyleString) {
476483
}
477484
}
478485

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

492518
// VisibleForTesting from {@link TextInputEventsTestCase}.
@@ -549,9 +575,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
549575
new SpannableStringBuilder(reactTextUpdate.getText());
550576

551577
manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments);
552-
553-
// Mitigation for https://github.com/facebook/react-native/issues/35936 (S318090)
554-
stripAbsoluteSizeSpans(spannableStringBuilder);
578+
stripStyleEquivalentSpans(spannableStringBuilder);
555579

556580
mContainsImages = reactTextUpdate.containsImages();
557581

@@ -626,24 +650,163 @@ private void manageSpans(
626650
}
627651
}
628652

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

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

640-
for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) {
641-
if (span.getSize() != effectiveFontSize) {
642-
continue outerLoop;
643-
}
694+
stripSpansOfKind(
695+
sb,
696+
ReactStrikethroughSpan.class,
697+
new SpanPredicate<ReactStrikethroughSpan>() {
698+
@Override
699+
public boolean test(ReactStrikethroughSpan span) {
700+
return (getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0;
701+
}
702+
});
703+
704+
stripSpansOfKind(
705+
sb,
706+
ReactUnderlineSpan.class,
707+
new SpanPredicate<ReactUnderlineSpan>() {
708+
@Override
709+
public boolean test(ReactUnderlineSpan span) {
710+
return (getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0;
711+
}
712+
});
713+
714+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
715+
stripSpansOfKind(
716+
sb,
717+
CustomLetterSpacingSpan.class,
718+
new SpanPredicate<CustomLetterSpacingSpan>() {
719+
@Override
720+
public boolean test(CustomLetterSpacingSpan span) {
721+
return span.getSpacing() == mTextAttributes.getEffectiveLetterSpacing();
722+
}
723+
});
724+
}
725+
726+
stripSpansOfKind(
727+
sb,
728+
CustomStyleSpan.class,
729+
new SpanPredicate<CustomStyleSpan>() {
730+
@Override
731+
public boolean test(CustomStyleSpan span) {
732+
return span.getStyle() == mFontStyle
733+
&& Objects.equals(span.getFontFamily(), mFontFamily)
734+
&& span.getWeight() == mFontWeight
735+
&& Objects.equals(span.getFontFeatureSettings(), getFontFeatureSettings());
736+
}
737+
});
738+
}
739+
740+
private <T> void stripSpansOfKind(
741+
SpannableStringBuilder sb, Class<T> clazz, SpanPredicate<T> shouldStrip) {
742+
T[] spans = sb.getSpans(0, sb.length(), clazz);
743+
744+
for (T span : spans) {
745+
if (shouldStrip.test(span)) {
746+
sb.removeSpan(span);
644747
}
748+
}
749+
}
645750

646-
sb.removeSpan(span);
751+
/**
752+
* Copy back styles represented as attributes to the underlying span, for later measurement
753+
* outside the ReactEditText.
754+
*/
755+
private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
756+
int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
757+
758+
// Set all bits for SPAN_PRIORITY so that this span has the highest possible priority
759+
// (least precedence). This ensures the span is behind any overlapping spans.
760+
spanFlags |= Spannable.SPAN_PRIORITY;
761+
762+
workingText.setSpan(
763+
new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()),
764+
0,
765+
workingText.length(),
766+
spanFlags);
767+
768+
workingText.setSpan(
769+
new ReactForegroundColorSpan(getCurrentTextColor()), 0, workingText.length(), spanFlags);
770+
771+
int backgroundColor = mReactBackgroundManager.getBackgroundColor();
772+
if (backgroundColor != Color.TRANSPARENT) {
773+
workingText.setSpan(
774+
new ReactBackgroundColorSpan(backgroundColor), 0, workingText.length(), spanFlags);
775+
}
776+
777+
if ((getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
778+
workingText.setSpan(new ReactStrikethroughSpan(), 0, workingText.length(), spanFlags);
779+
}
780+
781+
if ((getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0) {
782+
workingText.setSpan(new ReactUnderlineSpan(), 0, workingText.length(), spanFlags);
783+
}
784+
785+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
786+
float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
787+
if (!Float.isNaN(effectiveLetterSpacing)) {
788+
workingText.setSpan(
789+
new CustomLetterSpacingSpan(effectiveLetterSpacing),
790+
0,
791+
workingText.length(),
792+
spanFlags);
793+
}
794+
}
795+
796+
if (mFontStyle != UNSET
797+
|| mFontWeight != UNSET
798+
|| mFontFamily != null
799+
|| getFontFeatureSettings() != null) {
800+
workingText.setSpan(
801+
new CustomStyleSpan(
802+
mFontStyle,
803+
mFontWeight,
804+
getFontFeatureSettings(),
805+
mFontFamily,
806+
getContext().getAssets()),
807+
0,
808+
workingText.length(),
809+
spanFlags);
647810
}
648811
}
649812

@@ -989,7 +1152,9 @@ protected void applyTextAttributes() {
9891152

9901153
float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
9911154
if (!Float.isNaN(effectiveLetterSpacing)) {
992-
setLetterSpacing(effectiveLetterSpacing);
1155+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1156+
setLetterSpacing(effectiveLetterSpacing);
1157+
}
9931158
}
9941159
}
9951160

@@ -1062,6 +1227,7 @@ private void updateCachedSpannable(boolean resetStyles) {
10621227
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
10631228
try {
10641229
sb.append(currentText.subSequence(0, currentText.length()));
1230+
restoreStyleEquivalentSpans(sb);
10651231
} catch (IndexOutOfBoundsException e) {
10661232
ReactSoftExceptionLogger.logSoftException(TAG, e);
10671233
}

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

Lines changed: 21 additions & 0 deletions
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;
@@ -396,6 +398,11 @@ public void setFontStyle(ReactEditText view, @Nullable String fontStyle) {
396398
view.setFontStyle(fontStyle);
397399
}
398400

401+
@ReactProp(name = ViewProps.FONT_VARIANT)
402+
public void setFontVariant(ReactEditText view, @Nullable ReadableArray fontVariant) {
403+
view.setFontFeatureSettings(ReactTypefaceUtils.parseFontVariant(fontVariant));
404+
}
405+
399406
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
400407
public void setIncludeFontPadding(ReactEditText view, boolean includepad) {
401408
view.setIncludeFontPadding(includepad);
@@ -903,6 +910,20 @@ public void setAutoFocus(ReactEditText view, boolean autoFocus) {
903910
view.setAutoFocus(autoFocus);
904911
}
905912

913+
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
914+
public void setTextDecorationLine(ReactEditText view, @Nullable String textDecorationLineString) {
915+
view.setPaintFlags(
916+
view.getPaintFlags() & ~(Paint.STRIKE_THRU_TEXT_FLAG | Paint.UNDERLINE_TEXT_FLAG));
917+
918+
for (String token : textDecorationLineString.split(" ")) {
919+
if (token.equals("underline")) {
920+
view.setPaintFlags(view.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
921+
} else if (token.equals("line-through")) {
922+
view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
923+
}
924+
}
925+
}
926+
906927
@ReactPropGroup(
907928
names = {
908929
ViewProps.BORDER_WIDTH,

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

Lines changed: 5 additions & 0 deletions
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
}

scripts/test-manual-e2e.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,22 +113,22 @@ init_template_app(){
113113

114114
success "Preparing version $PACKAGE_VERSION"
115115

116-
npm pack
117-
118116
TIMESTAMP=$(date +%s)
119117
PACKAGE=$(pwd)/react-native-$PACKAGE_VERSION-$TIMESTAMP.tgz
120-
success "Package bundled ($PACKAGE)"
121-
122-
mv "$(pwd)/react-native-$PACKAGE_VERSION.tgz" "$PACKAGE"
123118

124119
node scripts/set-rn-template-version.js "file:$PACKAGE"
125120
success "React Native version changed in the template"
126121

122+
npm pack
123+
success "Package bundled ($PACKAGE)"
124+
125+
mv "$(pwd)/react-native-$PACKAGE_VERSION.tgz" "$PACKAGE"
126+
127127
project_name="RNTestProject"
128128

129129
cd /tmp/ || exit
130130
rm -rf "$project_name"
131-
node "$repo_root/cli.js" init "$project_name" --template "$repo_root"
131+
node "$repo_root/cli.js" init "$project_name" --template "$PACKAGE"
132132

133133
info "Double checking the versions in package.json are correct:"
134134
grep "\"react-native\": \".*react-native-$PACKAGE_VERSION-$TIMESTAMP.tgz\"" "/tmp/${project_name}/package.json" || error "Incorrect version number in /tmp/${project_name}/package.json"

0 commit comments

Comments
 (0)