diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt
new file mode 100644
index 00000000000000..e25f3d0564737f
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt
@@ -0,0 +1,14 @@
+package com.facebook.react.common
+
+/**
+ * General-purpose integer constants
+ */
+internal object IntConstants {
+ /**
+ * Some types have built-in support for representing a "missing" or "unset" value, for example
+ * NaN in the case of floating point numbers or null in the case of object references. Integers
+ * don't have such a special value. When an integer represent an inherently non-negative value,
+ * we use a special negative value to mark it as "unset".
+ */
+ const val UNSET: Int = -1
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java
index 7e7df01bd140ff..f2fc361501a7ff 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java
@@ -15,6 +15,8 @@
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.facebook.infer.annotation.Nullsafe;
+import com.facebook.react.common.IntConstants;
+
import java.util.HashMap;
import java.util.Map;
@@ -167,8 +169,6 @@ public static class TypefaceStyle {
public static final int BOLD = 700;
public static final int NORMAL = 400;
- public static final int UNSET = -1;
-
private static final int MIN_WEIGHT = 1;
private static final int MAX_WEIGHT = 1000;
@@ -177,11 +177,11 @@ public static class TypefaceStyle {
public TypefaceStyle(int weight, boolean italic) {
mItalic = italic;
- mWeight = weight == UNSET ? NORMAL : weight;
+ mWeight = weight == IntConstants.UNSET ? NORMAL : weight;
}
public TypefaceStyle(int style) {
- if (style == UNSET) {
+ if (style == IntConstants.UNSET) {
style = Typeface.NORMAL;
}
@@ -194,12 +194,12 @@ public TypefaceStyle(int style) {
* existing weight bit in `style` will be used.
*/
public TypefaceStyle(int style, int weight) {
- if (style == UNSET) {
+ if (style == IntConstants.UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
- mWeight = weight == UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight;
+ mWeight = weight == IntConstants.UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight;
}
public int getNearestStyle() {
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java
index 250772c9015e91..ee4882d37f5963 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java
@@ -173,4 +173,7 @@ public class ReactFeatureFlags {
* when there is work to do.
*/
public static boolean enableOnDemandReactChoreographer = false;
+
+ /** Enables the new unified {@link android.text.Spannable} building logic. */
+ public static boolean enableSpannableBuildingUnification = false;
}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt
new file mode 100644
index 00000000000000..54b6591ebaf3da
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt
@@ -0,0 +1,41 @@
+package com.facebook.react.views.text
+
+import com.facebook.react.uimanager.ReactAccessibilityDelegate
+
+/**
+ * Interface for an entity providing basic text attributes of a text node/fragment. "Basic" means
+ * that they can be provided trivially, without processing the parent element.
+ */
+internal interface BasicTextAttributeProvider {
+ val role: ReactAccessibilityDelegate.Role?
+
+ val accessibilityRole: ReactAccessibilityDelegate.AccessibilityRole?
+
+ val isBackgroundColorSet: Boolean
+
+ val backgroundColor: Int
+
+ val isColorSet: Boolean
+
+ val color: Int
+
+ val fontStyle: Int
+
+ val fontWeight: Int
+
+ val fontFamily: String?
+
+ val fontFeatureSettings: String?
+
+ val isUnderlineTextDecorationSet: Boolean
+
+ val isLineThroughTextDecorationSet: Boolean
+
+ val textShadowOffsetDx: Float
+
+ val textShadowOffsetDy: Float
+
+ val textShadowRadius: Float
+
+ val textShadowColor: Int
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
index 51fd4dd0149c77..6704f2684d9567 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java
@@ -14,6 +14,7 @@
import android.text.style.MetricAffectingSpan;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.assets.ReactFontManager;
@Nullsafe(Nullsafe.Mode.LOCAL)
@@ -61,11 +62,11 @@ public void updateMeasureState(TextPaint paint) {
}
public int getStyle() {
- return mStyle == ReactFontManager.TypefaceStyle.UNSET ? Typeface.NORMAL : mStyle;
+ return mStyle == IntConstants.UNSET ? Typeface.NORMAL : mStyle;
}
public int getWeight() {
- return mWeight == ReactFontManager.TypefaceStyle.UNSET
+ return mWeight == IntConstants.UNSET
? ReactFontManager.TypefaceStyle.NORMAL
: mWeight;
}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt
new file mode 100644
index 00000000000000..a9179ce7c0cf7c
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt
@@ -0,0 +1,19 @@
+package com.facebook.react.views.text
+
+import com.facebook.react.common.IntConstants.UNSET
+
+/**
+ * Interface for an entity providing effective text attributes of a text node/fragment
+ */
+internal interface EffectiveTextAttributeProvider : BasicTextAttributeProvider {
+ val textTransform: TextTransform
+
+ val effectiveLetterSpacing: Float
+
+ /**
+ * @return The effective font size, or [UNSET] if not set
+ */
+ val effectiveFontSize: Int
+
+ val effectiveLineHeight: Float
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt
new file mode 100644
index 00000000000000..da8568e06330f3
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt
@@ -0,0 +1,54 @@
+package com.facebook.react.views.text
+
+import com.facebook.react.common.IntConstants
+
+/**
+ * Implementation of [EffectiveTextAttributeProvider] that provides effective text
+ * attributes based on a [ReactBaseTextShadowNode] instance and its parent.
+ */
+internal class HierarchicTextAttributeProvider(
+ private val textShadowNode: ReactBaseTextShadowNode,
+ private val parentTextAttributes: TextAttributes?,
+ private val textAttributes: TextAttributes
+) : EffectiveTextAttributeProvider, BasicTextAttributeProvider by textShadowNode {
+ override val textTransform: TextTransform
+ get() = textAttributes.textTransform
+
+ override val effectiveLetterSpacing: Float
+ get() {
+ val letterSpacing = textAttributes.effectiveLetterSpacing
+
+ val isParentLetterSpacingDifferent =
+ parentTextAttributes == null || parentTextAttributes.effectiveLetterSpacing != letterSpacing
+
+ return if (!letterSpacing.isNaN() && isParentLetterSpacingDifferent) {
+ letterSpacing
+ } else {
+ Float.NaN
+ }
+ }
+
+ override val effectiveFontSize: Int
+ get() {
+ val fontSize = textAttributes.effectiveFontSize
+
+ return if (parentTextAttributes == null || parentTextAttributes.effectiveFontSize != fontSize) {
+ fontSize
+ } else {
+ IntConstants.UNSET
+ }
+ }
+
+ override val effectiveLineHeight: Float
+ get() {
+ val lineHeight = textAttributes.effectiveLineHeight
+ val isParentLineHeightDifferent =
+ parentTextAttributes == null || parentTextAttributes.effectiveLineHeight != lineHeight
+
+ return if (!lineHeight.isNaN() && isParentLineHeightDifferent) {
+ lineHeight
+ } else {
+ Float.NaN
+ }
+ }
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
index c752e04be06458..8249cd73b2d02d 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
@@ -20,8 +20,9 @@
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.ReactConstants;
-import com.facebook.react.common.assets.ReactFontManager;
+import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
@@ -49,13 +50,12 @@
*
This also node calculates {@link Spannable} object based on subnodes of the same type, which
* can be used in concrete classes to feed native views and compute layout.
*/
-public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
+public abstract class ReactBaseTextShadowNode extends LayoutShadowNode implements BasicTextAttributeProvider {
// Use a direction weak character so the placeholder doesn't change the direction of the previous
// character.
// https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters
private static final String INLINE_VIEW_PLACEHOLDER = "0";
- public static final int UNSET = ReactFontManager.TypefaceStyle.UNSET;
public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
@@ -70,6 +70,37 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
private static void buildSpannedFromShadowNode(
+ ReactBaseTextShadowNode textShadowNode,
+ SpannableStringBuilder sb,
+ List ops,
+ @Nullable TextAttributes parentTextAttributes,
+ boolean supportsInlineViews,
+ @Nullable Map 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 ops,
@@ -93,7 +124,7 @@ private static void buildSpannedFromShadowNode(
TextTransform.apply(
((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform()));
} else if (child instanceof ReactBaseTextShadowNode) {
- buildSpannedFromShadowNode(
+ buildSpannedFromShadowNodeDuplicated(
(ReactBaseTextShadowNode) child,
sb,
ops,
@@ -177,8 +208,8 @@ private static void buildSpannedFromShadowNode(
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
}
- if (textShadowNode.mFontStyle != UNSET
- || textShadowNode.mFontWeight != UNSET
+ if (textShadowNode.mFontStyle != IntConstants.UNSET
+ || textShadowNode.mFontWeight != IntConstants.UNSET
|| textShadowNode.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -221,6 +252,93 @@ private static void buildSpannedFromShadowNode(
}
}
+ private static void buildSpannedFromShadowNodeUnified(
+ ReactBaseTextShadowNode textShadowNode,
+ SpannableStringBuilder sb,
+ List ops,
+ @Nullable TextAttributes parentTextAttributes,
+ boolean supportsInlineViews,
+ @Nullable Map inlineViews,
+ int start) {
+
+ TextAttributes textAttributes;
+ if (parentTextAttributes != null) {
+ textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes);
+ } else {
+ 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) {
+ TextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider);
+ } else if (child instanceof ReactBaseTextShadowNode) {
+ buildSpannedFromShadowNodeUnified(
+ (ReactBaseTextShadowNode) child,
+ sb,
+ ops,
+ textAttributes,
+ supportsInlineViews,
+ inlineViews,
+ sb.length());
+ } else if (child instanceof ReactTextInlineImageShadowNode) {
+ addInlineImageSpan(ops, sb, (ReactTextInlineImageShadowNode) child);
+ } else if (supportsInlineViews) {
+ addInlineViewPlaceholderSpan(ops, sb, child);
+
+ inlineViews.put(child.getReactTag(), child);
+ } else {
+ throw new IllegalViewOperationException(
+ "Unexpected view type nested under a or node: " + child.getClass());
+ }
+ child.markUpdateSeen();
+ }
+ int end = sb.length();
+ if (end >= start) {
+ final int reactTag = textShadowNode.getReactTag();
+
+ TextLayoutUtils.addApplicableTextAttributeSpans(
+ ops, textAttributeProvider, reactTag, textShadowNode.getThemedContext(), start, end);
+ }
+ }
+
+ private static void addInlineImageSpan(List 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 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,
@@ -307,7 +425,7 @@ protected Spannable spannedFromShadowNode(
protected @Nullable AccessibilityRole mAccessibilityRole = null;
protected @Nullable Role mRole = null;
- protected int mNumberOfLines = UNSET;
+ protected int mNumberOfLines = IntConstants.UNSET;
protected int mTextAlign = Gravity.NO_GRAVITY;
protected int mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
protected int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
@@ -329,9 +447,9 @@ protected Spannable spannedFromShadowNode(
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link
* Typeface#NORMAL} or {@link Typeface#BOLD}.
*/
- protected int mFontStyle = UNSET;
+ protected int mFontStyle = IntConstants.UNSET;
- protected int mFontWeight = UNSET;
+ protected int mFontWeight = IntConstants.UNSET;
/**
* NB: If a font family is used that does not have a style in a certain Android version (ie.
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
@@ -384,9 +502,9 @@ private int getTextAlign() {
return textAlign;
}
- @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = UNSET)
+ @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = IntConstants.UNSET)
public void setNumberOfLines(int numberOfLines) {
- mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
+ mNumberOfLines = numberOfLines == 0 ? IntConstants.UNSET : numberOfLines;
markUpdated();
}
@@ -452,6 +570,11 @@ public void setFontSize(float fontSize) {
markUpdated();
}
+ @Override
+ public int getColor() {
+ return mColor;
+ }
+
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
@@ -461,6 +584,16 @@ public void setColor(@Nullable Integer color) {
markUpdated();
}
+ @Override
+ public boolean isColorSet() {
+ return mIsColorSet;
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
@ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color")
public void setBackgroundColor(@Nullable Integer color) {
// Background color needs to be handled here for virtual nodes so it can be incorporated into
@@ -476,6 +609,16 @@ public void setBackgroundColor(@Nullable Integer color) {
}
}
+ @Override
+ public boolean isBackgroundColorSet() {
+ return mIsBackgroundColorSet;
+ }
+
+ @Override
+ public @Nullable AccessibilityRole getAccessibilityRole() {
+ return mAccessibilityRole;
+ }
+
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
public void setAccessibilityRole(@Nullable String accessibilityRole) {
if (isVirtual()) {
@@ -484,6 +627,11 @@ public void setAccessibilityRole(@Nullable String accessibilityRole) {
}
}
+ @Override
+ public @Nullable Role getRole() {
+ return mRole;
+ }
+
@ReactProp(name = ViewProps.ROLE)
public void setRole(@Nullable String role) {
if (isVirtual()) {
@@ -492,12 +640,22 @@ public void setRole(@Nullable String role) {
}
}
+ @Override
+ public String getFontFamily() {
+ return mFontFamily;
+ }
+
@ReactProp(name = ViewProps.FONT_FAMILY)
public void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
markUpdated();
}
+ @Override
+ public int getFontWeight() {
+ return mFontWeight;
+ }
+
@ReactProp(name = ViewProps.FONT_WEIGHT)
public void setFontWeight(@Nullable String fontWeightString) {
int fontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString);
@@ -517,6 +675,16 @@ public void setFontVariant(@Nullable ReadableArray fontVariantArray) {
}
}
+ @Override
+ public String getFontFeatureSettings() {
+ return mFontFeatureSettings;
+ }
+
+ @Override
+ public int getFontStyle() {
+ return mFontStyle;
+ }
+
@ReactProp(name = ViewProps.FONT_STYLE)
public void setFontStyle(@Nullable String fontStyleString) {
int fontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString);
@@ -547,6 +715,16 @@ public void setTextDecorationLine(@Nullable String textDecorationLineString) {
markUpdated();
}
+ @Override
+ public boolean isUnderlineTextDecorationSet() {
+ return mIsUnderlineTextDecorationSet;
+ }
+
+ @Override
+ public boolean isLineThroughTextDecorationSet() {
+ return mIsLineThroughTextDecorationSet;
+ }
+
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) {
@@ -584,6 +762,21 @@ public void setTextShadowOffset(ReadableMap offsetMap) {
markUpdated();
}
+ @Override
+ public float getTextShadowOffsetDx() {
+ return mTextShadowOffsetDx;
+ }
+
+ @Override
+ public float getTextShadowOffsetDy() {
+ return mTextShadowOffsetDy;
+ }
+
+ @Override
+ public float getTextShadowRadius() {
+ return mTextShadowRadius;
+ }
+
@ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1)
public void setTextShadowRadius(float textShadowRadius) {
if (textShadowRadius != mTextShadowRadius) {
@@ -592,6 +785,11 @@ public void setTextShadowRadius(float textShadowRadius) {
}
}
+ @Override
+ public int getTextShadowColor() {
+ return mTextShadowColor;
+ }
+
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
public void setTextShadowColor(int textShadowColor) {
if (textShadowColor != mTextShadowColor) {
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
index d53f18a5aa3289..0e7b5fd27fa0fa 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
@@ -23,6 +23,7 @@
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactShadowNode;
@@ -80,7 +81,7 @@ public long measure(
int minimumFontSize =
(int) Math.max(mMinimumFontScale * initialFontSize, PixelUtil.toPixelFromDIP(4));
while (currentFontSize > minimumFontSize
- && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines
+ && (mNumberOfLines != IntConstants.UNSET && layout.getLineCount() > mNumberOfLines
|| heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) {
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
// measurements
@@ -122,7 +123,7 @@ public long measure(
}
final int lineCount =
- mNumberOfLines == UNSET
+ mNumberOfLines == IntConstants.UNSET
? layout.getLineCount()
: Math.min(mNumberOfLines, layout.getLineCount());
@@ -327,7 +328,7 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
mPreparedSpannableText,
- UNSET,
+ IntConstants.UNSET,
mContainsImages,
getPadding(Spacing.START),
getPadding(Spacing.TOP),
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java
index 0802ec3622a1bd..1bfab32967cb93 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java
@@ -7,11 +7,11 @@
package com.facebook.react.views.text;
-import static com.facebook.react.views.text.TextAttributeProps.UNSET;
-
import android.text.Layout;
import android.text.Spannable;
+import com.facebook.react.common.IntConstants;
+
/**
* Class that contains the data needed for a text update. Used by both and
* VisibleForTesting from {@link TextInputEventsTestCase}.
@@ -67,10 +67,10 @@ public ReactTextUpdate(
text,
jsEventCounter,
containsImages,
- UNSET,
- UNSET,
- UNSET,
- UNSET,
+ IntConstants.UNSET,
+ IntConstants.UNSET,
+ IntConstants.UNSET,
+ IntConstants.UNSET,
textAlign,
textBreakStrategy,
justificationMode);
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
index 0c1aa6a19cd555..6c4d8f10574d32 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java
@@ -7,8 +7,6 @@
package com.facebook.react.views.text;
-import static com.facebook.react.views.text.TextAttributeProps.UNSET;
-
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -35,6 +33,7 @@
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactCompoundView;
@@ -376,10 +375,10 @@ public void setText(ReactTextUpdate update) {
// In Fabric padding is set by the update of Layout Metrics and not as part of the "setText"
// operation
// TODO T56559197: remove this condition when we migrate 100% to Fabric
- if (paddingLeft != UNSET
- && paddingTop != UNSET
- && paddingRight != UNSET
- && paddingBottom != UNSET) {
+ if (paddingLeft != IntConstants.UNSET
+ && paddingTop != IntConstants.UNSET
+ && paddingRight != IntConstants.UNSET
+ && paddingBottom != IntConstants.UNSET) {
setPadding(
(int) Math.floor(paddingLeft),
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java
index 64fe5e36c0eb0d..da103426a9f5f4 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java
@@ -13,6 +13,7 @@
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.assets.ReactFontManager;
import java.util.ArrayList;
import java.util.List;
@@ -45,7 +46,7 @@ public static int parseFontWeight(@Nullable String fontWeightString) {
return 900;
}
}
- return ReactFontManager.TypefaceStyle.UNSET;
+ return IntConstants.UNSET;
}
public static int parseFontStyle(@Nullable String fontStyleString) {
@@ -57,7 +58,7 @@ public static int parseFontStyle(@Nullable String fontStyleString) {
return Typeface.NORMAL;
}
}
- return ReactFontManager.TypefaceStyle.UNSET;
+ return IntConstants.UNSET;
}
public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) {
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java
index 5df16e4e283822..57649508d54316 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java
@@ -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;
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
index aa450d79fd2111..df384e7d616fc6 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java
@@ -12,10 +12,12 @@
import android.text.TextUtils;
import android.util.LayoutDirection;
import android.view.Gravity;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.uimanager.PixelUtil;
@@ -30,7 +32,7 @@
// TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents
// TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to
// TextAttributeProps (e.g. TextAlign)
-public class TextAttributeProps {
+public class TextAttributeProps implements EffectiveTextAttributeProvider {
// constants for Text Attributes serialization
public static final short TA_KEY_FOREGROUND_COLOR = 0;
@@ -61,8 +63,6 @@ public class TextAttributeProps {
public static final short TA_KEY_ROLE = 26;
public static final short TA_KEY_TEXT_TRANSFORM = 27;
- public static final int UNSET = -1;
-
private static final String PROP_SHADOW_OFFSET = "textShadowOffset";
private static final String PROP_SHADOW_OFFSET_WIDTH = "width";
private static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
@@ -84,16 +84,17 @@ public class TextAttributeProps {
protected boolean mIsBackgroundColorSet = false;
protected int mBackgroundColor;
- protected int mNumberOfLines = UNSET;
- protected int mFontSize = UNSET;
- protected float mFontSizeInput = UNSET;
- protected float mLineHeightInput = UNSET;
+ protected int mNumberOfLines = IntConstants.UNSET;
+ protected int mFontSize = IntConstants.UNSET;
+ protected float mFontSizeInput = IntConstants.UNSET;
+ protected float mLineHeightInput = IntConstants.UNSET;
protected float mLetterSpacingInput = Float.NaN;
protected int mTextAlign = Gravity.NO_GRAVITY;
- // `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available.
- protected int mLayoutDirection = UNSET;
+ // `IntConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol)
+ protected int mLayoutDirection = IntConstants.UNSET;
+ @NonNull
protected TextTransform mTextTransform = TextTransform.NONE;
protected float mTextShadowOffsetDx = 0;
@@ -108,8 +109,8 @@ public class TextAttributeProps {
protected @Nullable AccessibilityRole mAccessibilityRole = null;
protected @Nullable Role mRole = null;
- protected int mFontStyle = UNSET;
- protected int mFontWeight = UNSET;
+ protected int mFontStyle = IntConstants.UNSET;
+ protected int mFontWeight = IntConstants.UNSET;
/**
* NB: If a font family is used that does not have a style in a certain Android version (ie.
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
@@ -233,11 +234,11 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) {
public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) {
TextAttributeProps result = new TextAttributeProps();
- result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, UNSET));
- result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, UNSET));
+ result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, IntConstants.UNSET));
+ result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, IntConstants.UNSET));
result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN));
result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true));
- result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, UNSET));
+ result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, IntConstants.UNSET));
result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
result.setColor(
props.hasKey(ViewProps.FOREGROUND_COLOR)
@@ -343,6 +344,7 @@ private static float getFloatProp(ReactStylesDiffMap mProps, String name, float
// Returns a line height which takes into account the requested line height
// and the height of the inline images.
+ @Override
public float getEffectiveLineHeight() {
boolean useInlineViewHeight =
!Float.isNaN(mLineHeight)
@@ -352,12 +354,12 @@ public float getEffectiveLineHeight() {
}
private void setNumberOfLines(int numberOfLines) {
- mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
+ mNumberOfLines = numberOfLines == 0 ? IntConstants.UNSET : numberOfLines;
}
private void setLineHeight(float lineHeight) {
mLineHeightInput = lineHeight;
- if (lineHeight == UNSET) {
+ if (lineHeight == IntConstants.UNSET) {
mLineHeight = Float.NaN;
} else {
mLineHeight =
@@ -371,6 +373,12 @@ private void setLetterSpacing(float letterSpacing) {
mLetterSpacingInput = letterSpacing;
}
+ @Override
+ @NonNull
+ public TextTransform getTextTransform() {
+ return mTextTransform;
+ }
+
public float getLetterSpacing() {
float letterSpacingPixels =
mAllowFontScaling
@@ -386,6 +394,16 @@ public float getLetterSpacing() {
return letterSpacingPixels / mFontSize;
}
+ @Override
+ public float getEffectiveLetterSpacing() {
+ return getLetterSpacing();
+ }
+
+ @Override
+ public int getEffectiveFontSize() {
+ return mFontSize;
+ }
+
private void setAllowFontScaling(boolean allowFontScaling) {
if (allowFontScaling != mAllowFontScaling) {
mAllowFontScaling = allowFontScaling;
@@ -397,7 +415,7 @@ private void setAllowFontScaling(boolean allowFontScaling) {
private void setFontSize(float fontSize) {
mFontSizeInput = fontSize;
- if (fontSize != UNSET) {
+ if (fontSize != IntConstants.UNSET) {
fontSize =
mAllowFontScaling
? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
@@ -406,6 +424,11 @@ private void setFontSize(float fontSize) {
mFontSize = (int) fontSize;
}
+ @Override
+ public int getColor() {
+ return mColor;
+ }
+
private void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
if (mIsColorSet) {
@@ -413,6 +436,16 @@ private void setColor(@Nullable Integer color) {
}
}
+ @Override
+ public boolean isColorSet() {
+ return mIsColorSet;
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
private void setBackgroundColor(Integer color) {
// TODO: Don't apply background color to anchor TextView since it will be applied on the View
// directly
@@ -424,6 +457,21 @@ private void setBackgroundColor(Integer color) {
// }
}
+ @Override
+ public boolean isBackgroundColorSet() {
+ return mIsBackgroundColorSet;
+ }
+
+ @Override
+ public int getFontStyle() {
+ return mFontStyle;
+ }
+
+ @Override
+ public String getFontFamily() {
+ return mFontFamily;
+ }
+
private void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
}
@@ -526,6 +574,16 @@ private void setFontVariant(@Nullable MapBuffer fontVariant) {
mFontFeatureSettings = TextUtils.join(", ", features);
}
+ @Override
+ public String getFontFeatureSettings() {
+ return mFontFeatureSettings;
+ }
+
+ @Override
+ public int getFontWeight() {
+ return mFontWeight;
+ }
+
private void setFontWeight(@Nullable String fontWeightString) {
mFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString);
}
@@ -552,6 +610,16 @@ private void setTextDecorationLine(@Nullable String textDecorationLineString) {
}
}
+ @Override
+ public boolean isUnderlineTextDecorationSet() {
+ return mIsUnderlineTextDecorationSet;
+ }
+
+ @Override
+ public boolean isLineThroughTextDecorationSet() {
+ return mIsLineThroughTextDecorationSet;
+ }
+
private void setTextShadowOffset(ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
mTextShadowOffsetDy = 0;
@@ -570,10 +638,20 @@ private void setTextShadowOffset(ReadableMap offsetMap) {
}
}
+ @Override
+ public float getTextShadowOffsetDx() {
+ return mTextShadowOffsetDx;
+ }
+
private void setTextShadowOffsetDx(float dx) {
mTextShadowOffsetDx = PixelUtil.toPixelFromDIP(dx);
}
+ @Override
+ public float getTextShadowOffsetDy() {
+ return mTextShadowOffsetDy;
+ }
+
private void setTextShadowOffsetDy(float dy) {
mTextShadowOffsetDy = PixelUtil.toPixelFromDIP(dy);
}
@@ -581,14 +659,14 @@ private void setTextShadowOffsetDy(float dy) {
public static int getLayoutDirection(@Nullable String layoutDirection) {
int androidLayoutDirection;
if (layoutDirection == null || "undefined".equals(layoutDirection)) {
- androidLayoutDirection = UNSET;
+ androidLayoutDirection = IntConstants.UNSET;
} else if ("rtl".equals(layoutDirection)) {
androidLayoutDirection = LayoutDirection.RTL;
} else if ("ltr".equals(layoutDirection)) {
androidLayoutDirection = LayoutDirection.LTR;
} else {
FLog.w(ReactConstants.TAG, "Invalid layoutDirection: " + layoutDirection);
- androidLayoutDirection = UNSET;
+ androidLayoutDirection = IntConstants.UNSET;
}
return androidLayoutDirection;
}
@@ -597,12 +675,22 @@ private void setLayoutDirection(@Nullable String layoutDirection) {
mLayoutDirection = getLayoutDirection(layoutDirection);
}
+ @Override
+ public float getTextShadowRadius() {
+ return mTextShadowRadius;
+ }
+
private void setTextShadowRadius(float textShadowRadius) {
if (textShadowRadius != mTextShadowRadius) {
mTextShadowRadius = textShadowRadius;
}
}
+ @Override
+ public int getTextShadowColor() {
+ return mTextShadowColor;
+ }
+
private void setTextShadowColor(int textShadowColor) {
if (textShadowColor != mTextShadowColor) {
mTextShadowColor = textShadowColor;
@@ -624,6 +712,11 @@ private void setTextTransform(@Nullable String textTransform) {
}
}
+ @Override
+ public AccessibilityRole getAccessibilityRole() {
+ return mAccessibilityRole;
+ }
+
private void setAccessibilityRole(@Nullable String accessibilityRole) {
if (accessibilityRole == null) {
mAccessibilityRole = null;
@@ -632,6 +725,12 @@ private void setAccessibilityRole(@Nullable String accessibilityRole) {
}
}
+ @Nullable
+ @Override
+ public Role getRole() {
+ return mRole;
+ }
+
private void setRole(@Nullable String role) {
if (role == null) {
mRole = null;
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java
index 8d54a23bbdf6ac..e944d4fb7586e0 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java
@@ -7,6 +7,7 @@
package com.facebook.react.views.text;
+import androidx.annotation.NonNull;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.PixelUtil;
@@ -29,6 +30,8 @@ public class TextAttributes {
private float mLetterSpacing = Float.NaN;
private float mMaxFontSizeMultiplier = Float.NaN;
private float mHeightOfTallestInlineViewOrImage = Float.NaN;
+
+ @NonNull
private TextTransform mTextTransform = TextTransform.UNSET;
public TextAttributes() {}
@@ -118,7 +121,7 @@ public TextTransform getTextTransform() {
return mTextTransform;
}
- public void setTextTransform(TextTransform textTransform) {
+ public void setTextTransform(@NonNull TextTransform textTransform) {
mTextTransform = textTransform;
}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java
index 93eda5d06b3866..7f70ca933371c9 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java
@@ -7,8 +7,6 @@
package com.facebook.react.views.text;
-import static com.facebook.react.views.text.TextAttributeProps.UNSET;
-
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
@@ -31,12 +29,15 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.build.ReactBuildConfig;
+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.views.text.fragments.BridgeTextFragmentList;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
@@ -99,7 +100,19 @@ public static void deleteCachedSpannableForTag(int reactTag) {
sTagToSpannableCache.remove(reactTag);
}
- private static void buildSpannableFromFragment(
+ private static void buildSpannableFromFragments(
+ Context context,
+ ReadableArray fragments,
+ SpannableStringBuilder sb,
+ List ops) {
+ if (ReactFeatureFlags.enableSpannableBuildingUnification) {
+ buildSpannableFromFragmentsUnified(context, fragments, sb, ops);
+ } else {
+ buildSpannableFromFragmentsDuplicated(context, fragments, sb, ops);
+ }
+ }
+
+ private static void buildSpannableFromFragmentsDuplicated(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
@@ -152,8 +165,8 @@ private static void buildSpannableFromFragment(
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
- if (textAttributes.mFontStyle != UNSET
- || textAttributes.mFontWeight != UNSET
+ if (textAttributes.mFontStyle != IntConstants.UNSET
+ || textAttributes.mFontWeight != IntConstants.UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -197,6 +210,17 @@ private static void buildSpannableFromFragment(
}
}
+ private static void buildSpannableFromFragmentsUnified(
+ Context context,
+ ReadableArray fragments,
+ SpannableStringBuilder sb,
+ List ops) {
+
+ final var textFragmentList = new BridgeTextFragmentList(fragments);
+
+ TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops);
+ }
+
// public because both ReactTextViewManager and ReactTextInputManager need to use this
public static Spannable getOrCreateSpannableForText(
Context context,
@@ -219,7 +243,7 @@ private static Spannable createSpannableFromAttributedString(
// a new spannable will be wiped out
List ops = new ArrayList<>();
- buildSpannableFromFragment(context, attributedString.getArray("fragments"), sb, ops);
+ buildSpannableFromFragments(context, attributedString.getArray("fragments"), sb, ops);
// TODO T31905686: add support for inline Images
// While setting the Spans on the final text, we also check whether any of them are images.
@@ -368,10 +392,10 @@ public static long measureText(
int maximumNumberOfLines =
paragraphAttributes.hasKey(MAXIMUM_NUMBER_OF_LINES_KEY)
? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY)
- : UNSET;
+ : IntConstants.UNSET;
int calculatedLineCount =
- maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
+ maximumNumberOfLines == IntConstants.UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
: Math.min(maximumNumberOfLines, layout.getLineCount());
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
index 60ef5f2e28de9d..9fb6c92c0d5954 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java
@@ -8,7 +8,6 @@
package com.facebook.react.views.text;
import static com.facebook.react.config.ReactFeatureFlags.enableTextSpannableCache;
-import static com.facebook.react.views.text.TextAttributeProps.UNSET;
import android.content.Context;
import android.graphics.Color;
@@ -29,12 +28,15 @@
import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
+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.views.text.fragments.MapBufferTextFragmentList;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
@@ -123,7 +125,16 @@ public static boolean isRTL(MapBuffer attributedString) {
== LayoutDirection.RTL;
}
- private static void buildSpannableFromFragment(
+ private static void buildSpannableFromFragments(
+ Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) {
+ if (ReactFeatureFlags.enableSpannableBuildingUnification) {
+ buildSpannableFromFragmentsUnified(context, fragments, sb, ops);
+ } else {
+ buildSpannableFromFragmentsDuplicated(context, fragments, sb, ops);
+ }
+ }
+
+ private static void buildSpannableFromFragmentsDuplicated(
Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) {
for (int i = 0, length = fragments.getCount(); i < length; i++) {
@@ -172,8 +183,8 @@ private static void buildSpannableFromFragment(
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
- if (textAttributes.mFontStyle != UNSET
- || textAttributes.mFontWeight != UNSET
+ if (textAttributes.mFontStyle != IntConstants.UNSET
+ || textAttributes.mFontWeight != IntConstants.UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -217,6 +228,14 @@ private static void buildSpannableFromFragment(
}
}
+ private static void buildSpannableFromFragmentsUnified(
+ Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) {
+
+ final var textFragmentList = new MapBufferTextFragmentList(fragments);
+
+ TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops);
+ }
+
// public because both ReactTextViewManager and ReactTextInputManager need to use this
public static Spannable getOrCreateSpannableForText(
Context context,
@@ -260,7 +279,7 @@ private static Spannable createSpannableFromAttributedString(
// a new spannable will be wiped out
List ops = new ArrayList<>();
- buildSpannableFromFragment(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops);
+ buildSpannableFromFragments(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops);
// TODO T31905686: add support for inline Images
// While setting the Spans on the final text, we also check whether any of them are images.
@@ -390,10 +409,10 @@ public static long measureText(
int maximumNumberOfLines =
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
- : UNSET;
+ : IntConstants.UNSET;
int calculatedLineCount =
- maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
+ maximumNumberOfLines == IntConstants.UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
: Math.min(maximumNumberOfLines, layout.getLineCount());
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt
new file mode 100644
index 00000000000000..c32a3fdaacbe1f
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.react.views.text
+
+import android.content.Context
+import android.graphics.Color
+import android.text.*
+import android.view.View
+import com.facebook.react.common.IntConstants
+import com.facebook.react.uimanager.PixelUtil
+import com.facebook.react.uimanager.ReactAccessibilityDelegate
+import com.facebook.react.views.text.fragments.TextFragmentList
+
+/**
+ * Utility methods for building [Spannable]s
+ */
+internal object TextLayoutUtils {
+ private const val INLINE_VIEW_PLACEHOLDER = "0"
+
+ @JvmStatic
+ fun buildSpannableFromTextFragmentList(
+ context: Context,
+ textFragmentList: TextFragmentList,
+ sb: SpannableStringBuilder,
+ ops: MutableList,
+ ) {
+
+ for (i in 0 until textFragmentList.count) {
+ val fragment = textFragmentList.getFragment(i)
+ val start = sb.length
+
+ // ReactRawText
+ val textAttributes = fragment.textAttributeProps
+
+ addText(sb, fragment.string, textAttributes)
+
+ val end = sb.length
+ val reactTag = if (fragment.hasReactTag()) fragment.reactTag else View.NO_ID
+ if (fragment.hasIsAttachment() && fragment.isAttachment) {
+ val width = PixelUtil.toPixelFromSP(fragment.width)
+ val height = PixelUtil.toPixelFromSP(fragment.height)
+
+ addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height)
+ } else if (end >= start) {
+ addApplicableTextAttributeSpans(ops, textAttributes, reactTag, context, start, end)
+ }
+ }
+ }
+
+ @JvmStatic
+ fun addText(
+ sb: SpannableStringBuilder, text: String?, textAttributeProvider: EffectiveTextAttributeProvider
+ ) {
+ sb.append(TextTransform.apply(text, textAttributeProvider.textTransform))
+ }
+
+ @JvmStatic
+ fun addInlineViewPlaceholderSpan(
+ ops: MutableList,
+ sb: SpannableStringBuilder,
+ reactTag: Int,
+ width: Float,
+ height: Float
+ ) {
+ ops.add(
+ SetSpanOperation(
+ sb.length - INLINE_VIEW_PLACEHOLDER.length,
+ sb.length,
+ TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt())
+ )
+ )
+ }
+
+ @JvmStatic
+ fun addApplicableTextAttributeSpans(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ reactTag: Int,
+ context: Context,
+ start: Int,
+ end: Int
+ ) {
+ addColorSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addBackgroundColorSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addLinkSpanIfApplicable(ops, textAttributeProvider, reactTag, start, end)
+
+ addLetterSpacingSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addFontSizeSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addCustomStyleSpanIfApplicable(ops, textAttributeProvider, context, start, end)
+
+ addUnderlineSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addStrikethroughSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addShadowStyleSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addLineHeightSpanIfApplicable(ops, textAttributeProvider, start, end)
+
+ addReactTagSpan(ops, start, end, reactTag)
+ }
+
+ @JvmStatic
+ private fun addLinkSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ reactTag: Int,
+ start: Int,
+ end: Int
+ ) {
+ val roleIsLink =
+ textAttributeProvider.role?.let { it == ReactAccessibilityDelegate.Role.LINK }
+ ?: (textAttributeProvider.accessibilityRole == ReactAccessibilityDelegate.AccessibilityRole.LINK)
+ if (roleIsLink) {
+ ops.add(SetSpanOperation(start, end, ReactClickableSpan(reactTag)))
+ }
+ }
+
+ @JvmStatic
+ private fun addColorSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ if (textAttributeProvider.isColorSet) {
+ ops.add(
+ SetSpanOperation(
+ start, end, ReactForegroundColorSpan(textAttributeProvider.color)
+ )
+ )
+ }
+ }
+
+ @JvmStatic
+ private fun addBackgroundColorSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ if (textAttributeProvider.isBackgroundColorSet) {
+ ops.add(
+ SetSpanOperation(
+ start, end, ReactBackgroundColorSpan(textAttributeProvider.backgroundColor)
+ )
+ )
+ }
+ }
+
+ @JvmStatic
+ private fun addLetterSpacingSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ val effectiveLetterSpacing = textAttributeProvider.effectiveLetterSpacing
+
+ if (!effectiveLetterSpacing.isNaN()) {
+ ops.add(SetSpanOperation(start, end, CustomLetterSpacingSpan(effectiveLetterSpacing)))
+ }
+ }
+
+ @JvmStatic
+ private fun addFontSizeSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ val effectiveFontSize = textAttributeProvider.effectiveFontSize
+
+ if (effectiveFontSize != IntConstants.UNSET) {
+ ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize)))
+ }
+ }
+
+
+ @JvmStatic
+ private fun addCustomStyleSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ context: Context,
+ start: Int,
+ end: Int
+ ) {
+ val fontStyle = textAttributeProvider.fontStyle
+ val fontWeight = textAttributeProvider.fontWeight
+ val fontFamily = textAttributeProvider.fontFamily
+
+ if (fontStyle != IntConstants.UNSET || fontWeight != IntConstants.UNSET || fontFamily != null) {
+ ops.add(
+ SetSpanOperation(
+ start, end, CustomStyleSpan(
+ fontStyle,
+ fontWeight,
+ textAttributeProvider.fontFeatureSettings,
+ fontFamily,
+ context.assets
+ )
+ )
+ )
+ }
+ }
+
+ @JvmStatic
+ private fun addUnderlineSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ if (textAttributeProvider.isUnderlineTextDecorationSet) {
+ ops.add(SetSpanOperation(start, end, ReactUnderlineSpan()))
+ }
+ }
+
+ @JvmStatic
+ private fun addStrikethroughSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ if (textAttributeProvider.isLineThroughTextDecorationSet) {
+ ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan()))
+ }
+ }
+
+ @JvmStatic
+ private fun addShadowStyleSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ val hasTextShadowOffset =
+ textAttributeProvider.textShadowOffsetDx != 0f || textAttributeProvider.textShadowOffsetDy != 0f
+ val hasTextShadowRadius = textAttributeProvider.textShadowRadius != 0f
+ val hasTextShadowColorAlpha = Color.alpha(textAttributeProvider.textShadowColor) != 0
+
+ if ((hasTextShadowOffset || hasTextShadowRadius) && hasTextShadowColorAlpha) {
+ ops.add(
+ SetSpanOperation(
+ start, end, ShadowStyleSpan(
+ textAttributeProvider.textShadowOffsetDx,
+ textAttributeProvider.textShadowOffsetDy,
+ textAttributeProvider.textShadowRadius,
+ textAttributeProvider.textShadowColor
+ )
+ )
+ )
+ }
+ }
+
+
+ @JvmStatic
+ private fun addLineHeightSpanIfApplicable(
+ ops: MutableList,
+ textAttributeProvider: EffectiveTextAttributeProvider,
+ start: Int,
+ end: Int
+ ) {
+ val effectiveLineHeight = textAttributeProvider.effectiveLineHeight
+ if (!effectiveLineHeight.isNaN()) {
+ ops.add(SetSpanOperation(start, end, CustomLineHeightSpan(effectiveLineHeight)))
+ }
+ }
+
+
+ @JvmStatic
+ private fun addReactTagSpan(
+ ops: MutableList, start: Int, end: Int, reactTag: Int
+ ) {
+ ops.add(SetSpanOperation(start, end, ReactTagSpan(reactTag)))
+ }
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java
index ad96b7d4437b76..e4f1f37bfd9740 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java
@@ -8,6 +8,7 @@
package com.facebook.react.views.text;
import java.text.BreakIterator;
+import androidx.annotation.Nullable;
/** Types of text transforms for CustomTextTransformSpan */
public enum TextTransform {
@@ -17,7 +18,7 @@ public enum TextTransform {
CAPITALIZE,
UNSET;
- public static String apply(String text, TextTransform textTransform) {
+ public static String apply(@Nullable String text, TextTransform textTransform) {
if (text == null) {
return null;
}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt
new file mode 100644
index 00000000000000..fd09104d8f1f0c
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt
@@ -0,0 +1,33 @@
+package com.facebook.react.views.text.fragments
+
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.uimanager.ReactStylesDiffMap
+import com.facebook.react.uimanager.ViewProps
+import com.facebook.react.views.text.TextAttributeProps
+
+/**
+ * A [TextFragment] implementation backed by a a [ReadableMap]
+ */
+internal class BridgeTextFragment(private val fragment: ReadableMap) : TextFragment {
+ override val textAttributeProps: TextAttributeProps
+ get() = TextAttributeProps.fromReadableMap(ReactStylesDiffMap(fragment.getMap("textAttributes")))
+
+ override val string: String?
+ get() = fragment.getString("string")
+
+ override fun hasReactTag(): Boolean = fragment.hasKey("reactTag")
+
+ override val reactTag: Int
+ get() = fragment.getInt("reactTag")
+
+ override fun hasIsAttachment(): Boolean = fragment.hasKey(ViewProps.IS_ATTACHMENT)
+
+ override val isAttachment: Boolean
+ get() = fragment.getBoolean(ViewProps.IS_ATTACHMENT)
+
+ override val width: Double
+ get() = fragment.getDouble(ViewProps.WIDTH)
+
+ override val height: Double
+ get() = fragment.getDouble(ViewProps.HEIGHT)
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt
new file mode 100644
index 00000000000000..b57622fc72a6f4
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt
@@ -0,0 +1,13 @@
+package com.facebook.react.views.text.fragments
+
+import com.facebook.react.bridge.ReadableArray
+
+/**
+ * A list of [TextFragment]s backed by a [ReadableArray]
+ */
+internal class BridgeTextFragmentList(private val fragments: ReadableArray) : TextFragmentList {
+ override fun getFragment(index: Int): TextFragment = BridgeTextFragment(fragments.getMap(index))
+
+ override val count: Int
+ get() = fragments.size()
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt
new file mode 100644
index 00000000000000..6e11e7a996fb4e
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt
@@ -0,0 +1,38 @@
+package com.facebook.react.views.text.fragments
+
+import com.facebook.react.common.mapbuffer.MapBuffer
+import com.facebook.react.views.text.TextAttributeProps
+
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_HEIGHT
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_IS_ATTACHMENT
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_REACT_TAG
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_STRING
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_TEXT_ATTRIBUTES
+import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_WIDTH
+
+/**
+ * A [TextFragment] implementation backed by a [MapBuffer]
+ */
+internal class MapBufferTextFragment(private val fragment: MapBuffer) : TextFragment {
+ override val textAttributeProps: TextAttributeProps
+ get() = TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES.toInt()))
+
+ override val string: String
+ get() = fragment.getString(FR_KEY_STRING.toInt())
+
+ override fun hasReactTag(): Boolean = fragment.contains(FR_KEY_REACT_TAG.toInt())
+
+ override val reactTag: Int
+ get() = fragment.getInt(FR_KEY_REACT_TAG.toInt())
+
+ override fun hasIsAttachment(): Boolean = fragment.contains(FR_KEY_IS_ATTACHMENT.toInt())
+
+ override val isAttachment: Boolean
+ get() = fragment.getBoolean(FR_KEY_IS_ATTACHMENT.toInt())
+
+ override val width: Double
+ get() = fragment.getDouble(FR_KEY_WIDTH.toInt())
+
+ override val height: Double
+ get() = fragment.getDouble(FR_KEY_HEIGHT.toInt())
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt
new file mode 100644
index 00000000000000..75f76cefc41a78
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt
@@ -0,0 +1,14 @@
+package com.facebook.react.views.text.fragments
+
+import com.facebook.react.common.mapbuffer.MapBuffer
+
+/**
+ * A list of [TextFragment]s backed by a [MapBuffer]
+ */
+internal class MapBufferTextFragmentList(private val fragments: MapBuffer) : TextFragmentList {
+ override fun getFragment(index: Int): TextFragment =
+ MapBufferTextFragment(fragments.getMapBuffer(index))
+
+ override val count: Int
+ get() = fragments.count
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt
new file mode 100644
index 00000000000000..7d86c345fe06f3
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt
@@ -0,0 +1,24 @@
+package com.facebook.react.views.text.fragments
+
+import com.facebook.react.views.text.TextAttributeProps
+
+/**
+ * Interface for a text fragment
+ */
+internal interface TextFragment {
+ val textAttributeProps: TextAttributeProps
+
+ val string: String?
+
+ fun hasReactTag(): Boolean
+
+ val reactTag: Int
+
+ fun hasIsAttachment(): Boolean
+
+ val isAttachment: Boolean
+
+ val width: Double
+
+ val height: Double
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt
new file mode 100644
index 00000000000000..b50ed43cb646e3
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt
@@ -0,0 +1,10 @@
+package com.facebook.react.views.text.fragments
+
+/**
+ * Interface for a list of [TextFragment]s
+ */
+internal interface TextFragmentList {
+ fun getFragment(index: Int): TextFragment
+
+ val count: Int
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
index 1c7950e4b3c006..014bd48093cc20 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
@@ -46,6 +46,7 @@
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.StateWrapper;
@@ -96,8 +97,6 @@ public class ReactEditText extends AppCompatEditText {
/** A count of events sent to JS or C++. */
protected int mNativeEventCount;
- private static final int UNSET = -1;
-
private @Nullable ArrayList mListeners;
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
private int mStagedInputType;
@@ -114,8 +113,8 @@ public class ReactEditText extends AppCompatEditText {
private TextAttributes mTextAttributes;
private boolean mTypefaceDirty = false;
private @Nullable String mFontFamily = null;
- private int mFontWeight = UNSET;
- private int mFontStyle = UNSET;
+ private int mFontWeight = IntConstants.UNSET;
+ private int mFontStyle = IntConstants.UNSET;
private boolean mAutoFocus = false;
private boolean mDidAttachToWindow = false;
private @Nullable String mPlaceholder = null;
@@ -379,7 +378,7 @@ public void maybeSetSelection(int eventCounter, int start, int end) {
return;
}
- if (start != UNSET && end != UNSET) {
+ if (start != IntConstants.UNSET && end != IntConstants.UNSET) {
// clamp selection values for safety
start = clampToTextLength(start);
end = clampToTextLength(end);
@@ -588,8 +587,8 @@ public void maybeUpdateTypeface() {
// Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
// nonstandard
- if (mFontStyle != UNSET
- || mFontWeight != UNSET
+ if (mFontStyle != IntConstants.UNSET
+ || mFontWeight != IntConstants.UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
@@ -819,8 +818,8 @@ private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) {
new CustomLetterSpacingSpan(effectiveLetterSpacing), 0, workingText.length(), spanFlags);
}
- if (mFontStyle != UNSET
- || mFontWeight != UNSET
+ if (mFontStyle != IntConstants.UNSET
+ || mFontWeight != IntConstants.UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
workingText.setSpan(
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java
index 878da5199174ca..0b904002445559 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java
@@ -17,6 +17,7 @@
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
+import com.facebook.react.common.IntConstants;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.Spacing;
@@ -36,7 +37,7 @@
public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
implements YogaMeasureFunction {
- private int mMostRecentEventCount = UNSET;
+ private int mMostRecentEventCount = IntConstants.UNSET;
private @Nullable EditText mInternalEditText;
private @Nullable ReactTextInputLocalData mLocalData;
@@ -108,7 +109,7 @@ public long measure(
} else {
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.getEffectiveFontSize());
- if (mNumberOfLines != UNSET) {
+ if (mNumberOfLines != IntConstants.UNSET) {
editText.setLines(mNumberOfLines);
}
@@ -191,7 +192,7 @@ public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
super.onCollectExtraUpdates(uiViewOperationQueue);
- if (mMostRecentEventCount != UNSET) {
+ if (mMostRecentEventCount != IntConstants.UNSET) {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
spannedFromShadowNode(