Skip to content

Commit

Permalink
Merge pull request #37084 from facebook/kelset/068-backport-samsung-f…
Browse files Browse the repository at this point in the history
…ixes
  • Loading branch information
kelset authored Apr 26, 2023
2 parents 91a3a54 + e89f919 commit 558bc33
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public void updateMeasureState(TextPaint paint) {
apply(paint);
}

public float getSpacing() {
return mLetterSpacing;
}

private void apply(TextPaint paint) {
if (!Float.isNaN(mLetterSpacing)) {
paint.setLetterSpacing(mLetterSpacing);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public int getWeight() {
return mFontFamily;
}

public @Nullable String getFontFeatureSettings() {
return mFeatureSettings;
}

private static void apply(
Paint paint,
int style,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import static com.facebook.react.views.text.TextAttributeProps.UNSET;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
Expand Down Expand Up @@ -50,15 +52,20 @@
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.ReactStrikethroughSpan;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTypefaceUtils;
import com.facebook.react.views.text.ReactUnderlineSpan;
import com.facebook.react.views.text.TextAttributes;
import com.facebook.react.views.text.TextInlineImageSpan;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.react.views.view.ReactViewBackgroundManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* A wrapper around the EditText that lets us better control what happens when an EditText gets
Expand Down Expand Up @@ -476,6 +483,14 @@ public void setFontStyle(String fontStyleString) {
}
}

@Override
public void setFontFeatureSettings(String fontFeatureSettings) {
if (!Objects.equals(fontFeatureSettings, getFontFeatureSettings())) {
super.setFontFeatureSettings(fontFeatureSettings);
mTypefaceDirty = true;
}
}

public void maybeUpdateTypeface() {
if (!mTypefaceDirty) {
return;
Expand All @@ -487,6 +502,17 @@ public void maybeUpdateTypeface() {
ReactTypefaceUtils.applyStyles(
getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets());
setTypeface(newTypeface);

// Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
// nonstandard
if (mFontStyle != UNSET
|| mFontWeight != UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
} else {
setPaintFlags(getPaintFlags() & (~Paint.SUBPIXEL_TEXT_FLAG));
}
}

// VisibleForTesting from {@link TextInputEventsTestCase}.
Expand Down Expand Up @@ -549,9 +575,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)
stripAbsoluteSizeSpans(spannableStringBuilder);
stripStyleEquivalentSpans(spannableStringBuilder);

mContainsImages = reactTextUpdate.containsImages();

Expand Down Expand Up @@ -626,24 +650,163 @@ private void manageSpans(
}
}

private void stripAbsoluteSizeSpans(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);
}

/**
* 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();
}
});

stripSpansOfKind(
sb,
ReactBackgroundColorSpan.class,
new SpanPredicate<ReactBackgroundColorSpan>() {
@Override
public boolean test(ReactBackgroundColorSpan span) {
return span.getBackgroundColor() == mReactBackgroundManager.getBackgroundColor();
}
});

outerLoop:
for (ReactAbsoluteSizeSpan span : spans) {
ReactAbsoluteSizeSpan[] overlappingSpans =
sb.getSpans(sb.getSpanStart(span), sb.getSpanEnd(span), ReactAbsoluteSizeSpan.class);
stripSpansOfKind(
sb,
ReactForegroundColorSpan.class,
new SpanPredicate<ReactForegroundColorSpan>() {
@Override
public boolean test(ReactForegroundColorSpan span) {
return span.getForegroundColor() == getCurrentTextColor();
}
});

for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) {
if (span.getSize() != effectiveFontSize) {
continue outerLoop;
}
stripSpansOfKind(
sb,
ReactStrikethroughSpan.class,
new SpanPredicate<ReactStrikethroughSpan>() {
@Override
public boolean test(ReactStrikethroughSpan span) {
return (getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0;
}
});

stripSpansOfKind(
sb,
ReactUnderlineSpan.class,
new SpanPredicate<ReactUnderlineSpan>() {
@Override
public boolean test(ReactUnderlineSpan span) {
return (getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0;
}
});

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
stripSpansOfKind(
sb,
CustomLetterSpacingSpan.class,
new SpanPredicate<CustomLetterSpacingSpan>() {
@Override
public boolean test(CustomLetterSpacingSpan span) {
return span.getSpacing() == mTextAttributes.getEffectiveLetterSpacing();
}
});
}

stripSpansOfKind(
sb,
CustomStyleSpan.class,
new SpanPredicate<CustomStyleSpan>() {
@Override
public boolean test(CustomStyleSpan span) {
return span.getStyle() == mFontStyle
&& Objects.equals(span.getFontFamily(), mFontFamily)
&& span.getWeight() == mFontWeight
&& Objects.equals(span.getFontFeatureSettings(), getFontFeatureSettings());
}
});
}

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

for (T span : spans) {
if (shouldStrip.test(span)) {
sb.removeSpan(span);
}
}
}

sb.removeSpan(span);
/**
* 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
// (least precedence). This ensures the span is behind any overlapping spans.
spanFlags |= Spannable.SPAN_PRIORITY;

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

workingText.setSpan(
new ReactForegroundColorSpan(getCurrentTextColor()), 0, workingText.length(), spanFlags);

int backgroundColor = mReactBackgroundManager.getBackgroundColor();
if (backgroundColor != Color.TRANSPARENT) {
workingText.setSpan(
new ReactBackgroundColorSpan(backgroundColor), 0, workingText.length(), spanFlags);
}

if ((getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
workingText.setSpan(new ReactStrikethroughSpan(), 0, workingText.length(), spanFlags);
}

if ((getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0) {
workingText.setSpan(new ReactUnderlineSpan(), 0, workingText.length(), spanFlags);
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
if (!Float.isNaN(effectiveLetterSpacing)) {
workingText.setSpan(
new CustomLetterSpacingSpan(effectiveLetterSpacing),
0,
workingText.length(),
spanFlags);
}
}

if (mFontStyle != UNSET
|| mFontWeight != UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
workingText.setSpan(
new CustomStyleSpan(
mFontStyle,
mFontWeight,
getFontFeatureSettings(),
mFontFamily,
getContext().getAssets()),
0,
workingText.length(),
spanFlags);
}
}

Expand Down Expand Up @@ -989,7 +1152,9 @@ protected void applyTextAttributes() {

float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing();
if (!Float.isNaN(effectiveLetterSpacing)) {
setLetterSpacing(effectiveLetterSpacing);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLetterSpacing(effectiveLetterSpacing);
}
}
}

Expand Down Expand Up @@ -1062,6 +1227,7 @@ private void updateCachedSpannable(boolean resetStyles) {
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
try {
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 @@ -13,6 +13,7 @@
import android.content.res.ColorStateList;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
Expand Down Expand Up @@ -67,6 +68,7 @@
import com.facebook.react.views.text.ReactBaseTextShadowNode;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTextViewManagerCallback;
import com.facebook.react.views.text.ReactTypefaceUtils;
import com.facebook.react.views.text.TextAttributeProps;
import com.facebook.react.views.text.TextInlineImageSpan;
import com.facebook.react.views.text.TextLayoutManager;
Expand Down Expand Up @@ -396,6 +398,11 @@ public void setFontStyle(ReactEditText view, @Nullable String fontStyle) {
view.setFontStyle(fontStyle);
}

@ReactProp(name = ViewProps.FONT_VARIANT)
public void setFontVariant(ReactEditText view, @Nullable ReadableArray fontVariant) {
view.setFontFeatureSettings(ReactTypefaceUtils.parseFontVariant(fontVariant));
}

@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
public void setIncludeFontPadding(ReactEditText view, boolean includepad) {
view.setIncludeFontPadding(includepad);
Expand Down Expand Up @@ -903,6 +910,20 @@ public void setAutoFocus(ReactEditText view, boolean autoFocus) {
view.setAutoFocus(autoFocus);
}

@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
public void setTextDecorationLine(ReactEditText view, @Nullable String textDecorationLineString) {
view.setPaintFlags(
view.getPaintFlags() & ~(Paint.STRIKE_THRU_TEXT_FLAG | Paint.UNDERLINE_TEXT_FLAG));

for (String token : textDecorationLineString.split(" ")) {
if (token.equals("underline")) {
view.setPaintFlags(view.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
} else if (token.equals("line-through")) {
view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
}
}

@ReactPropGroup(
names = {
ViewProps.BORDER_WIDTH,
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 @@ -50,6 +51,10 @@ public void setBackgroundColor(int color) {
}
}

public int getBackgroundColor() {
return mColor;
}

public void setBorderWidth(int position, float width) {
getOrCreateReactViewBackground().setBorderWidth(position, width);
}
Expand Down
12 changes: 6 additions & 6 deletions scripts/test-manual-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ init_template_app(){

success "Preparing version $PACKAGE_VERSION"

npm pack

TIMESTAMP=$(date +%s)
PACKAGE=$(pwd)/react-native-$PACKAGE_VERSION-$TIMESTAMP.tgz
success "Package bundled ($PACKAGE)"

mv "$(pwd)/react-native-$PACKAGE_VERSION.tgz" "$PACKAGE"

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

npm pack
success "Package bundled ($PACKAGE)"

mv "$(pwd)/react-native-$PACKAGE_VERSION.tgz" "$PACKAGE"

project_name="RNTestProject"

cd /tmp/ || exit
rm -rf "$project_name"
node "$repo_root/cli.js" init "$project_name" --template "$repo_root"
node "$repo_root/cli.js" init "$project_name" --template "$PACKAGE"

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

0 comments on commit 558bc33

Please sign in to comment.