Skip to content

Commit

Permalink
Add a feature flag for the unified Spannable building logic
Browse files Browse the repository at this point in the history
...temporarily bringing back the old logic.
  • Loading branch information
cubuspl42 committed Oct 26, 2023
1 parent 3dbfe25 commit c0fcf72
Show file tree
Hide file tree
Showing 5 changed files with 421 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,7 @@ public class ReactFeatureFlags {
* priorities from any thread.
*/
public static boolean useModernRuntimeScheduler = false;

/** Enables the new unified {@link android.text.Spannable} building logic. */
public static boolean enableSpannableBuildingUnification = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.facebook.react.views.text;

import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout;
Expand All @@ -24,6 +25,7 @@
import com.facebook.react.internal.views.text.BasicTextAttributeProvider;
import com.facebook.react.internal.views.text.HierarchicTextAttributeProvider;
import com.facebook.react.internal.views.text.TextLayoutUtils;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
Expand Down Expand Up @@ -74,6 +76,189 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode implement
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;

private static void buildSpannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode,
SpannableStringBuilder sb,
List<SetSpanOperation> ops,
@Nullable TextAttributes parentTextAttributes,
boolean supportsInlineViews,
@Nullable Map<Integer, ReactShadowNode> inlineViews,
int start) {
if (ReactFeatureFlags.enableSpannableBuildingUnification) {
buildSpannedFromShadowNodeUnified(
textShadowNode,
sb,
ops,
parentTextAttributes,
supportsInlineViews,
inlineViews,
start
);
} else {
buildSpannedFromShadowNodeDuplicated(
textShadowNode,
sb,
ops,
parentTextAttributes,
supportsInlineViews,
inlineViews,
start
);
}
}

private static void buildSpannedFromShadowNodeDuplicated(
ReactBaseTextShadowNode textShadowNode,
SpannableStringBuilder sb,
List<SetSpanOperation> ops,
@Nullable TextAttributes parentTextAttributes,
boolean supportsInlineViews,
@Nullable Map<Integer, ReactShadowNode> inlineViews,
int start) {

TextAttributes textAttributes;
if (parentTextAttributes != null) {
textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes);
} else {
textAttributes = textShadowNode.mTextAttributes;
}

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()));
} else if (child instanceof ReactBaseTextShadowNode) {
buildSpannedFromShadowNodeDuplicated(
(ReactBaseTextShadowNode) child,
sb,
ops,
textAttributes,
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 (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;
}

// 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);
} else {
throw new IllegalViewOperationException(
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
}
child.markUpdateSeen();
}
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())));
}
}

private static void buildSpannedFromShadowNodeUnified(
ReactBaseTextShadowNode textShadowNode,
SpannableStringBuilder sb,
List<SetSpanOperation> ops,
Expand All @@ -97,7 +282,7 @@ private static void buildSpannedFromShadowNode(
if (child instanceof ReactRawTextShadowNode) {
sTextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider);
} else if (child instanceof ReactBaseTextShadowNode) {
buildSpannedFromShadowNode(
buildSpannedFromShadowNodeUnified(
(ReactBaseTextShadowNode) child,
sb,
ops,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,7 @@ public TextTransform getTextTransform() {
return mTextTransform;
}

@Override
public float getEffectiveLetterSpacing() {
public float getLetterSpacing() {
float letterSpacingPixels =
mAllowFontScaling
? PixelUtil.toPixelFromSP(mLetterSpacingInput)
Expand All @@ -394,6 +393,11 @@ public float getEffectiveLetterSpacing() {
return letterSpacingPixels / mFontSize;
}

@Override
public float getEffectiveLetterSpacing() {
return getLetterSpacing();
}

@Override
public int getEffectiveFontSize() {
return mFontSize;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static com.facebook.react.views.text.TextAttributeProps.UNSET;

import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
Expand All @@ -20,6 +21,7 @@
import android.text.TextPaint;
import android.util.LayoutDirection;
import android.util.LruCache;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
Expand All @@ -31,7 +33,11 @@
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.internal.views.text.TextLayoutUtils;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.internal.views.text.fragments.BridgeTextFragmentList;
import com.facebook.yoga.YogaConstants;
Expand Down Expand Up @@ -103,6 +109,116 @@ private static void buildSpannableFromFragment(
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
if (ReactFeatureFlags.enableSpannableBuildingUnification) {
buildSpannableFromFragmentUnified(context, fragments, sb, ops);
} else {
buildSpannableFromFragmentDuplicated(context, fragments, sb, ops);
}
}

private static void buildSpannableFromFragmentDuplicated(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {

for (int i = 0, length = fragments.size(); i < length; i++) {
ReadableMap fragment = fragments.getMap(i);
int start = sb.length();

// ReactRawText
TextAttributeProps textAttributes =
TextAttributeProps.fromReadableMap(
new ReactStylesDiffMap(fragment.getMap("textAttributes")));

sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform));

int end = sb.length();
int reactTag = fragment.hasKey("reactTag") ? fragment.getInt("reactTag") : View.NO_ID;
if (fragment.hasKey(ViewProps.IS_ATTACHMENT)
&& fragment.getBoolean(ViewProps.IS_ATTACHMENT)) {
float width = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.WIDTH));
float height = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.HEIGHT));
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
} else if (end >= start) {
boolean roleIsLink =
textAttributes.mRole != null
? textAttributes.mRole == Role.LINK
: textAttributes.mAccessibilityRole == AccessibilityRole.LINK;
if (roleIsLink) {
ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag)));
}
if (textAttributes.mIsColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactForegroundColorSpan(textAttributes.mColor)));
}
if (textAttributes.mIsBackgroundColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor)));
}
if (!Float.isNaN(textAttributes.getLetterSpacing())) {
ops.add(
new SetSpanOperation(
start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing())));
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomStyleSpan(
textAttributes.mFontStyle,
textAttributes.mFontWeight,
textAttributes.mFontFeatureSettings,
textAttributes.mFontFamily,
context.getAssets())));
}
if (textAttributes.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
}
if (textAttributes.mIsLineThroughTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
}
if ((textAttributes.mTextShadowOffsetDx != 0
|| textAttributes.mTextShadowOffsetDy != 0
|| textAttributes.mTextShadowRadius != 0)
&& Color.alpha(textAttributes.mTextShadowColor) != 0) {
ops.add(
new SetSpanOperation(
start,
end,
new ShadowStyleSpan(
textAttributes.mTextShadowOffsetDx,
textAttributes.mTextShadowOffsetDy,
textAttributes.mTextShadowRadius,
textAttributes.mTextShadowColor)));
}
if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) {
ops.add(
new SetSpanOperation(
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
}

ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
}
}
}

private static void buildSpannableFromFragmentUnified(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {

final var textFragmentList = new BridgeTextFragmentList(fragments);

Expand Down
Loading

0 comments on commit c0fcf72

Please sign in to comment.