Skip to content

Commit c0fcf72

Browse files
committed
Add a feature flag for the unified Spannable building logic
...temporarily bringing back the old logic.
1 parent 3dbfe25 commit c0fcf72

File tree

5 files changed

+421
-5
lines changed

5 files changed

+421
-5
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,7 @@ public class ReactFeatureFlags {
161161
* priorities from any thread.
162162
*/
163163
public static boolean useModernRuntimeScheduler = false;
164+
165+
/** Enables the new unified {@link android.text.Spannable} building logic. */
166+
public static boolean enableSpannableBuildingUnification = false;
164167
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.facebook.react.views.text;
99

10+
import android.graphics.Color;
1011
import android.graphics.Typeface;
1112
import android.os.Build;
1213
import android.text.Layout;
@@ -24,6 +25,7 @@
2425
import com.facebook.react.internal.views.text.BasicTextAttributeProvider;
2526
import com.facebook.react.internal.views.text.HierarchicTextAttributeProvider;
2627
import com.facebook.react.internal.views.text.TextLayoutUtils;
28+
import com.facebook.react.config.ReactFeatureFlags;
2729
import com.facebook.react.uimanager.IllegalViewOperationException;
2830
import com.facebook.react.uimanager.LayoutShadowNode;
2931
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
@@ -74,6 +76,189 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode implement
7476
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
7577

7678
private static void buildSpannedFromShadowNode(
79+
ReactBaseTextShadowNode textShadowNode,
80+
SpannableStringBuilder sb,
81+
List<SetSpanOperation> ops,
82+
@Nullable TextAttributes parentTextAttributes,
83+
boolean supportsInlineViews,
84+
@Nullable Map<Integer, ReactShadowNode> inlineViews,
85+
int start) {
86+
if (ReactFeatureFlags.enableSpannableBuildingUnification) {
87+
buildSpannedFromShadowNodeUnified(
88+
textShadowNode,
89+
sb,
90+
ops,
91+
parentTextAttributes,
92+
supportsInlineViews,
93+
inlineViews,
94+
start
95+
);
96+
} else {
97+
buildSpannedFromShadowNodeDuplicated(
98+
textShadowNode,
99+
sb,
100+
ops,
101+
parentTextAttributes,
102+
supportsInlineViews,
103+
inlineViews,
104+
start
105+
);
106+
}
107+
}
108+
109+
private static void buildSpannedFromShadowNodeDuplicated(
110+
ReactBaseTextShadowNode textShadowNode,
111+
SpannableStringBuilder sb,
112+
List<SetSpanOperation> ops,
113+
@Nullable TextAttributes parentTextAttributes,
114+
boolean supportsInlineViews,
115+
@Nullable Map<Integer, ReactShadowNode> inlineViews,
116+
int start) {
117+
118+
TextAttributes textAttributes;
119+
if (parentTextAttributes != null) {
120+
textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes);
121+
} else {
122+
textAttributes = textShadowNode.mTextAttributes;
123+
}
124+
125+
for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
126+
ReactShadowNode child = textShadowNode.getChildAt(i);
127+
128+
if (child instanceof ReactRawTextShadowNode) {
129+
sb.append(
130+
TextTransform.apply(
131+
((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform()));
132+
} else if (child instanceof ReactBaseTextShadowNode) {
133+
buildSpannedFromShadowNodeDuplicated(
134+
(ReactBaseTextShadowNode) child,
135+
sb,
136+
ops,
137+
textAttributes,
138+
supportsInlineViews,
139+
inlineViews,
140+
sb.length());
141+
} else if (child instanceof ReactTextInlineImageShadowNode) {
142+
// We make the image take up 1 character in the span and put a corresponding character into
143+
// the text so that the image doesn't run over any following text.
144+
sb.append(INLINE_VIEW_PLACEHOLDER);
145+
ops.add(
146+
new SetSpanOperation(
147+
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
148+
sb.length(),
149+
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
150+
} else if (supportsInlineViews) {
151+
int reactTag = child.getReactTag();
152+
YogaValue widthValue = child.getStyleWidth();
153+
YogaValue heightValue = child.getStyleHeight();
154+
155+
float width;
156+
float height;
157+
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
158+
// If the measurement of the child isn't calculated, we calculate the layout for the
159+
// view using Yoga
160+
child.calculateLayout();
161+
width = child.getLayoutWidth();
162+
height = child.getLayoutHeight();
163+
} else {
164+
width = widthValue.value;
165+
height = heightValue.value;
166+
}
167+
168+
// We make the inline view take up 1 character in the span and put a corresponding character
169+
// into
170+
// the text so that the inline view doesn't run over any following text.
171+
sb.append(INLINE_VIEW_PLACEHOLDER);
172+
ops.add(
173+
new SetSpanOperation(
174+
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
175+
sb.length(),
176+
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
177+
inlineViews.put(reactTag, child);
178+
} else {
179+
throw new IllegalViewOperationException(
180+
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
181+
}
182+
child.markUpdateSeen();
183+
}
184+
int end = sb.length();
185+
if (end >= start) {
186+
if (textShadowNode.mIsColorSet) {
187+
ops.add(
188+
new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor)));
189+
}
190+
if (textShadowNode.mIsBackgroundColorSet) {
191+
ops.add(
192+
new SetSpanOperation(
193+
start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor)));
194+
}
195+
boolean roleIsLink =
196+
textShadowNode.mRole != null
197+
? textShadowNode.mRole == Role.LINK
198+
: textShadowNode.mAccessibilityRole == AccessibilityRole.LINK;
199+
if (roleIsLink) {
200+
ops.add(
201+
new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag())));
202+
}
203+
float effectiveLetterSpacing = textAttributes.getEffectiveLetterSpacing();
204+
if (!Float.isNaN(effectiveLetterSpacing)
205+
&& (parentTextAttributes == null
206+
|| parentTextAttributes.getEffectiveLetterSpacing() != effectiveLetterSpacing)) {
207+
ops.add(
208+
new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing)));
209+
}
210+
int effectiveFontSize = textAttributes.getEffectiveFontSize();
211+
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything like
212+
// `Float.NaN`.
213+
parentTextAttributes == null
214+
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
215+
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
216+
}
217+
if (textShadowNode.mFontStyle != UNSET
218+
|| textShadowNode.mFontWeight != UNSET
219+
|| textShadowNode.mFontFamily != null) {
220+
ops.add(
221+
new SetSpanOperation(
222+
start,
223+
end,
224+
new CustomStyleSpan(
225+
textShadowNode.mFontStyle,
226+
textShadowNode.mFontWeight,
227+
textShadowNode.mFontFeatureSettings,
228+
textShadowNode.mFontFamily,
229+
textShadowNode.getThemedContext().getAssets())));
230+
}
231+
if (textShadowNode.mIsUnderlineTextDecorationSet) {
232+
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
233+
}
234+
if (textShadowNode.mIsLineThroughTextDecorationSet) {
235+
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
236+
}
237+
if ((textShadowNode.mTextShadowOffsetDx != 0
238+
|| textShadowNode.mTextShadowOffsetDy != 0
239+
|| textShadowNode.mTextShadowRadius != 0)
240+
&& Color.alpha(textShadowNode.mTextShadowColor) != 0) {
241+
ops.add(
242+
new SetSpanOperation(
243+
start,
244+
end,
245+
new ShadowStyleSpan(
246+
textShadowNode.mTextShadowOffsetDx,
247+
textShadowNode.mTextShadowOffsetDy,
248+
textShadowNode.mTextShadowRadius,
249+
textShadowNode.mTextShadowColor)));
250+
}
251+
float effectiveLineHeight = textAttributes.getEffectiveLineHeight();
252+
if (!Float.isNaN(effectiveLineHeight)
253+
&& (parentTextAttributes == null
254+
|| parentTextAttributes.getEffectiveLineHeight() != effectiveLineHeight)) {
255+
ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight)));
256+
}
257+
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
258+
}
259+
}
260+
261+
private static void buildSpannedFromShadowNodeUnified(
77262
ReactBaseTextShadowNode textShadowNode,
78263
SpannableStringBuilder sb,
79264
List<SetSpanOperation> ops,
@@ -97,7 +282,7 @@ private static void buildSpannedFromShadowNode(
97282
if (child instanceof ReactRawTextShadowNode) {
98283
sTextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider);
99284
} else if (child instanceof ReactBaseTextShadowNode) {
100-
buildSpannedFromShadowNode(
285+
buildSpannedFromShadowNodeUnified(
101286
(ReactBaseTextShadowNode) child,
102287
sb,
103288
ops,

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,7 @@ public TextTransform getTextTransform() {
378378
return mTextTransform;
379379
}
380380

381-
@Override
382-
public float getEffectiveLetterSpacing() {
381+
public float getLetterSpacing() {
383382
float letterSpacingPixels =
384383
mAllowFontScaling
385384
? PixelUtil.toPixelFromSP(mLetterSpacingInput)
@@ -394,6 +393,11 @@ public float getEffectiveLetterSpacing() {
394393
return letterSpacingPixels / mFontSize;
395394
}
396395

396+
@Override
397+
public float getEffectiveLetterSpacing() {
398+
return getLetterSpacing();
399+
}
400+
397401
@Override
398402
public int getEffectiveFontSize() {
399403
return mFontSize;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static com.facebook.react.views.text.TextAttributeProps.UNSET;
1111

1212
import android.content.Context;
13+
import android.graphics.Color;
1314
import android.os.Build;
1415
import android.text.BoringLayout;
1516
import android.text.Layout;
@@ -20,6 +21,7 @@
2021
import android.text.TextPaint;
2122
import android.util.LayoutDirection;
2223
import android.util.LruCache;
24+
import android.view.View;
2325
import androidx.annotation.NonNull;
2426
import androidx.annotation.Nullable;
2527
import com.facebook.common.logging.FLog;
@@ -31,7 +33,11 @@
3133
import com.facebook.react.bridge.WritableArray;
3234
import com.facebook.react.common.build.ReactBuildConfig;
3335
import com.facebook.react.internal.views.text.TextLayoutUtils;
36+
import com.facebook.react.config.ReactFeatureFlags;
3437
import com.facebook.react.uimanager.PixelUtil;
38+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
39+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
40+
import com.facebook.react.uimanager.ReactStylesDiffMap;
3541
import com.facebook.react.uimanager.ViewProps;
3642
import com.facebook.react.internal.views.text.fragments.BridgeTextFragmentList;
3743
import com.facebook.yoga.YogaConstants;
@@ -103,6 +109,116 @@ private static void buildSpannableFromFragment(
103109
ReadableArray fragments,
104110
SpannableStringBuilder sb,
105111
List<SetSpanOperation> ops) {
112+
if (ReactFeatureFlags.enableSpannableBuildingUnification) {
113+
buildSpannableFromFragmentUnified(context, fragments, sb, ops);
114+
} else {
115+
buildSpannableFromFragmentDuplicated(context, fragments, sb, ops);
116+
}
117+
}
118+
119+
private static void buildSpannableFromFragmentDuplicated(
120+
Context context,
121+
ReadableArray fragments,
122+
SpannableStringBuilder sb,
123+
List<SetSpanOperation> ops) {
124+
125+
for (int i = 0, length = fragments.size(); i < length; i++) {
126+
ReadableMap fragment = fragments.getMap(i);
127+
int start = sb.length();
128+
129+
// ReactRawText
130+
TextAttributeProps textAttributes =
131+
TextAttributeProps.fromReadableMap(
132+
new ReactStylesDiffMap(fragment.getMap("textAttributes")));
133+
134+
sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform));
135+
136+
int end = sb.length();
137+
int reactTag = fragment.hasKey("reactTag") ? fragment.getInt("reactTag") : View.NO_ID;
138+
if (fragment.hasKey(ViewProps.IS_ATTACHMENT)
139+
&& fragment.getBoolean(ViewProps.IS_ATTACHMENT)) {
140+
float width = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.WIDTH));
141+
float height = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.HEIGHT));
142+
ops.add(
143+
new SetSpanOperation(
144+
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
145+
sb.length(),
146+
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
147+
} else if (end >= start) {
148+
boolean roleIsLink =
149+
textAttributes.mRole != null
150+
? textAttributes.mRole == Role.LINK
151+
: textAttributes.mAccessibilityRole == AccessibilityRole.LINK;
152+
if (roleIsLink) {
153+
ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag)));
154+
}
155+
if (textAttributes.mIsColorSet) {
156+
ops.add(
157+
new SetSpanOperation(
158+
start, end, new ReactForegroundColorSpan(textAttributes.mColor)));
159+
}
160+
if (textAttributes.mIsBackgroundColorSet) {
161+
ops.add(
162+
new SetSpanOperation(
163+
start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor)));
164+
}
165+
if (!Float.isNaN(textAttributes.getLetterSpacing())) {
166+
ops.add(
167+
new SetSpanOperation(
168+
start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing())));
169+
}
170+
ops.add(
171+
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
172+
if (textAttributes.mFontStyle != UNSET
173+
|| textAttributes.mFontWeight != UNSET
174+
|| textAttributes.mFontFamily != null) {
175+
ops.add(
176+
new SetSpanOperation(
177+
start,
178+
end,
179+
new CustomStyleSpan(
180+
textAttributes.mFontStyle,
181+
textAttributes.mFontWeight,
182+
textAttributes.mFontFeatureSettings,
183+
textAttributes.mFontFamily,
184+
context.getAssets())));
185+
}
186+
if (textAttributes.mIsUnderlineTextDecorationSet) {
187+
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
188+
}
189+
if (textAttributes.mIsLineThroughTextDecorationSet) {
190+
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
191+
}
192+
if ((textAttributes.mTextShadowOffsetDx != 0
193+
|| textAttributes.mTextShadowOffsetDy != 0
194+
|| textAttributes.mTextShadowRadius != 0)
195+
&& Color.alpha(textAttributes.mTextShadowColor) != 0) {
196+
ops.add(
197+
new SetSpanOperation(
198+
start,
199+
end,
200+
new ShadowStyleSpan(
201+
textAttributes.mTextShadowOffsetDx,
202+
textAttributes.mTextShadowOffsetDy,
203+
textAttributes.mTextShadowRadius,
204+
textAttributes.mTextShadowColor)));
205+
}
206+
if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) {
207+
ops.add(
208+
new SetSpanOperation(
209+
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
210+
}
211+
212+
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
213+
}
214+
}
215+
}
216+
217+
private static void buildSpannableFromFragmentUnified(
218+
Context context,
219+
ReadableArray fragments,
220+
SpannableStringBuilder sb,
221+
List<SetSpanOperation> ops) {
106222

107223
final var textFragmentList = new BridgeTextFragmentList(fragments);
108224

0 commit comments

Comments
 (0)