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

De-duplicate building Spannable #39621

Closed
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
@@ -0,0 +1,52 @@
package com.facebook.react.views.text;

import com.facebook.react.common.assets.ReactFontManager;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;

public interface EffectiveTextAttributeProvider {
int UNSET = ReactFontManager.TypefaceStyle.UNSET;

TextTransform getTextTransform();

float getEffectiveLetterSpacing();

/**
* @return The effective font size, or {@link #UNSET} if not set
*/
int getEffectiveFontSize();

Role getRole();

ReactAccessibilityDelegate.AccessibilityRole getAccessibilityRole();

boolean isBackgroundColorSet();

int getBackgroundColor();

boolean isColorSet();

int getColor();

int getFontStyle();

int getFontWeight();

String getFontFamily();

String getFontFeatureSettings();

boolean isUnderlineTextDecorationSet();

boolean isLineThroughTextDecorationSet();

float getTextShadowOffsetDx();

float getTextShadowOffsetDy();

float getTextShadowRadius();

int getTextShadowColor();

float getEffectiveLineHeight();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package com.facebook.react.views.text;

import android.annotation.TargetApi;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout;
Expand Down Expand Up @@ -52,6 +51,141 @@
*/
@TargetApi(Build.VERSION_CODES.M)
public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
private static class HierarchicTextAttributeProvider implements EffectiveTextAttributeProvider {
final private ReactBaseTextShadowNode textShadowNode;
final private TextAttributes parentTextAttributes;
final private TextAttributes textAttributes;

private HierarchicTextAttributeProvider(ReactBaseTextShadowNode textShadowNode, TextAttributes parentTextAttributes, TextAttributes textAttributes) {
this.textShadowNode = textShadowNode;
this.parentTextAttributes = parentTextAttributes;
this.textAttributes = textAttributes;
}

@Override
public TextTransform getTextTransform() {
return textAttributes.getTextTransform();
}

@Override
public Role getRole() {
return textShadowNode.mRole;
}

@Override
public AccessibilityRole getAccessibilityRole() {
return textShadowNode.mAccessibilityRole;
}

@Override
public boolean isBackgroundColorSet() {
return textShadowNode.mIsBackgroundColorSet;
}

@Override
public int getBackgroundColor() {
return textShadowNode.mBackgroundColor;
}

@Override
public boolean isColorSet() {
return textShadowNode.mIsColorSet;
}

@Override
public int getColor() {
return textShadowNode.mColor;
}

@Override
public int getFontStyle() {
return textShadowNode.mFontStyle;
}

@Override
public int getFontWeight() {
return textShadowNode.mFontWeight;
}

@Override
public String getFontFamily() {
return textShadowNode.mFontFamily;
}

@Override
public String getFontFeatureSettings() {
return textShadowNode.mFontFeatureSettings;
}

@Override
public boolean isUnderlineTextDecorationSet() {
return textShadowNode.mIsUnderlineTextDecorationSet;
}

@Override
public boolean isLineThroughTextDecorationSet() {
return textShadowNode.mIsLineThroughTextDecorationSet;
}

@Override
public float getTextShadowOffsetDx() {
return textShadowNode.mTextShadowOffsetDx;
}

@Override
public float getTextShadowOffsetDy() {
return textShadowNode.mTextShadowOffsetDy;
}

@Override
public float getTextShadowRadius() {
return textShadowNode.mTextShadowRadius;
}

@Override
public int getTextShadowColor() {
return textShadowNode.mTextShadowColor;
}

@Override
public float getEffectiveLetterSpacing() {
final float letterSpacing = textAttributes.getEffectiveLetterSpacing();

if (!Float.isNaN(letterSpacing)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLetterSpacing() != letterSpacing)) {
return letterSpacing;
} else {
return Float.NaN;
}
}

@Override
public int getEffectiveFontSize() {
final int fontSize = textAttributes.getEffectiveFontSize();

if (
// `getEffectiveFontSize` always returns a value so don't need to check for anything like `Float.NaN`.
parentTextAttributes == null
|| parentTextAttributes.getEffectiveFontSize() != fontSize) {
return fontSize;
} else {
return UNSET;
}
}

@Override
public float getEffectiveLineHeight() {
final float lineHeight = textAttributes.getEffectiveLineHeight();
if (!Float.isNaN(lineHeight)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLineHeight() != lineHeight)) {
return lineHeight;
} else {
return Float.NaN;
}
}
}

// Use a direction weak character so the placeholder doesn't change the direction of the previous
// character.
Expand Down Expand Up @@ -87,13 +221,13 @@ private static void buildSpannedFromShadowNode(
textAttributes = textShadowNode.mTextAttributes;
}

final var textAttributeProvider = new HierarchicTextAttributeProvider(textShadowNode, parentTextAttributes, textAttributes);

for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
ReactShadowNode child = textShadowNode.getChildAt(i);

if (child instanceof ReactRawTextShadowNode) {
sb.append(
TextTransform.apply(
((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform()));
TextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider);
} else if (child instanceof ReactBaseTextShadowNode) {
buildSpannedFromShadowNode(
(ReactBaseTextShadowNode) child,
Expand All @@ -103,43 +237,12 @@ private static void buildSpannedFromShadowNode(
supportsInlineViews,
inlineViews,
sb.length());
} else if (child instanceof ReactTextInlineImageShadowNode) {
// We make the image take up 1 character in the span and put a corresponding character into
// the text so that the image doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
} else if (child instanceof ReactTextInlineImageShadowNode inlineImageChild) {
addInlineImageSpan(ops, sb, inlineImageChild);
} else if (supportsInlineViews) {
int reactTag = child.getReactTag();
YogaValue widthValue = child.getStyleWidth();
YogaValue heightValue = child.getStyleHeight();

float width;
float height;
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
// If the measurement of the child isn't calculated, we calculate the layout for the
// view using Yoga
child.calculateLayout();
width = child.getLayoutWidth();
height = child.getLayoutHeight();
} else {
width = widthValue.value;
height = heightValue.value;
}
addInlineViewPlaceholderSpan(ops, sb, child);

// We make the inline view take up 1 character in the span and put a corresponding character
// into
// the text so that the inline view doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
inlineViews.put(reactTag, child);
inlineViews.put(child.getReactTag(), child);
} else {
throw new IllegalViewOperationException(
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
Expand All @@ -148,81 +251,47 @@ private static void buildSpannedFromShadowNode(
}
int end = sb.length();
if (end >= start) {
if (textShadowNode.mIsColorSet) {
ops.add(
new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor)));
}
if (textShadowNode.mIsBackgroundColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor)));
}
boolean roleIsLink =
textShadowNode.mRole != null
? textShadowNode.mRole == Role.LINK
: textShadowNode.mAccessibilityRole == AccessibilityRole.LINK;
if (roleIsLink) {
ops.add(
new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag())));
}
float effectiveLetterSpacing = textAttributes.getEffectiveLetterSpacing();
if (!Float.isNaN(effectiveLetterSpacing)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLetterSpacing() != effectiveLetterSpacing)) {
ops.add(
new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing)));
}
int effectiveFontSize = textAttributes.getEffectiveFontSize();
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything like
// `Float.NaN`.
parentTextAttributes == null
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
}
if (textShadowNode.mFontStyle != UNSET
|| textShadowNode.mFontWeight != UNSET
|| textShadowNode.mFontFamily != null) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomStyleSpan(
textShadowNode.mFontStyle,
textShadowNode.mFontWeight,
textShadowNode.mFontFeatureSettings,
textShadowNode.mFontFamily,
textShadowNode.getThemedContext().getAssets())));
}
if (textShadowNode.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
}
if (textShadowNode.mIsLineThroughTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
}
if ((textShadowNode.mTextShadowOffsetDx != 0
|| textShadowNode.mTextShadowOffsetDy != 0
|| textShadowNode.mTextShadowRadius != 0)
&& Color.alpha(textShadowNode.mTextShadowColor) != 0) {
ops.add(
new SetSpanOperation(
start,
end,
new ShadowStyleSpan(
textShadowNode.mTextShadowOffsetDx,
textShadowNode.mTextShadowOffsetDy,
textShadowNode.mTextShadowRadius,
textShadowNode.mTextShadowColor)));
}
float effectiveLineHeight = textAttributes.getEffectiveLineHeight();
if (!Float.isNaN(effectiveLineHeight)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLineHeight() != effectiveLineHeight)) {
ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight)));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
final int reactTag = textShadowNode.getReactTag();

TextLayoutUtils.addApplicableTextAttributeSpans(
ops, textAttributeProvider, reactTag, textShadowNode.getThemedContext(), start, end);
}
}

private static void addInlineImageSpan(List<SetSpanOperation> ops, SpannableStringBuilder sb,
ReactTextInlineImageShadowNode child) {
// We make the image take up 1 character in the span and put a corresponding character into
// the text so that the image doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(new SetSpanOperation(sb.length() - INLINE_VIEW_PLACEHOLDER.length(), sb.length(),
child.buildInlineImageSpan()));
}

private static void addInlineViewPlaceholderSpan(List<SetSpanOperation> ops, SpannableStringBuilder sb,
ReactShadowNode child) {
YogaValue widthValue = child.getStyleWidth();
YogaValue heightValue = child.getStyleHeight();

float width;
float height;
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
// If the measurement of the child isn't calculated, we calculate the layout for the
// view using Yoga
child.calculateLayout();
width = child.getLayoutWidth();
height = child.getLayoutHeight();
} else {
width = widthValue.value;
height = heightValue.value;
}

// We make the inline view take up 1 character in the span and put a corresponding character into the text so that
// the inline view doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);

TextLayoutUtils.addInlineViewPlaceholderSpan(ops, sb, child.getReactTag(), width, height);
}

// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
protected Spannable spannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import android.text.Spanned;
import com.facebook.common.logging.FLog;

class SetSpanOperation {
public class SetSpanOperation {
private static final String TAG = "SetSpanOperation";
static final int SPAN_MAX_PRIORITY = Spanned.SPAN_PRIORITY >> Spanned.SPAN_PRIORITY_SHIFT;

Expand Down
Loading