De-duplicate building Spannable on Android (#39630)

Summary:
A first step in my work on https://github.com/react-native-community/discussions-and-proposals/issues/695

De-duplicate the code for creating `Spannable` on Android. I'm planning to add quite serious new features to this module. This would be really hard with the current level of code duplication.

## Changelog:

[INTERNAL] [CHANGED] - De-duplicate building `Spannable` on Android

Pull Request resolved: https://github.com/facebook/react-native/pull/39630

Test Plan: I tried to ensure that the refactored code is relatively easy to prove to be equivalent to the original duplicated one, but there's always a risk of a human mistake in this process. So far, I have been testing this by ensuring that nothing broke in the `Text` example section in RNTester.

Reviewed By: mdvacca

Differential Revision: D51016244

Pulled By: NickGerleman

fbshipit-source-id: e9f873c01b2af0685c7b0943aebea170c997d22e
This commit is contained in:
Jakub Trzebiatowski
2024-01-19 17:24:52 -08:00
committed by Facebook GitHub Bot
parent 6fbf042d93
commit 7bcdb23cf0
28 changed files with 1051 additions and 94 deletions
@@ -1686,6 +1686,7 @@ public final class com/facebook/react/common/MapBuilder$Builder {
public class com/facebook/react/common/ReactConstants {
public static final field TAG Ljava/lang/String;
public static final field UNSET I
public fun <init> ()V
}
@@ -1764,7 +1765,6 @@ public class com/facebook/react/common/assets/ReactFontManager {
public class com/facebook/react/common/assets/ReactFontManager$TypefaceStyle {
public static final field BOLD I
public static final field NORMAL I
public static final field UNSET I
public fun <init> (I)V
public fun <init> (II)V
public fun <init> (IZ)V
@@ -1906,6 +1906,7 @@ public class com/facebook/react/config/ReactFeatureFlags {
public static field enableMountHooks Z
public static field enableOnDemandReactChoreographer Z
public static field enableRemoveDeleteTreeInstruction Z
public static field enableSpannableBuildingUnification Z
public static field enableTextSpannableCache Z
public static field enableViewRecycling Z
public static field excludeYogaFromRawProps Z
@@ -6785,7 +6786,7 @@ public class com/facebook/react/views/text/ReactBackgroundColorSpan : android/te
public fun <init> (I)V
}
public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode {
public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode, com/facebook/react/views/text/BasicTextAttributeProvider {
public static final field DEFAULT_TEXT_SHADOW_COLOR I
public static final field PROP_SHADOW_COLOR Ljava/lang/String;
public static final field PROP_SHADOW_OFFSET Ljava/lang/String;
@@ -6793,7 +6794,6 @@ public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : co
public static final field PROP_SHADOW_OFFSET_WIDTH Ljava/lang/String;
public static final field PROP_SHADOW_RADIUS Ljava/lang/String;
public static final field PROP_TEXT_TRANSFORM Ljava/lang/String;
public static final field UNSET I
protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
protected field mAdjustsFontSizeToFit Z
protected field mBackgroundColor I
@@ -6824,6 +6824,22 @@ public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : co
protected field mTextShadowRadius F
public fun <init> ()V
public fun <init> (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
public fun getBackgroundColor ()I
public fun getColor ()I
public fun getFontFamily ()Ljava/lang/String;
public fun getFontFeatureSettings ()Ljava/lang/String;
public fun getFontStyle ()I
public fun getFontWeight ()I
public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
public fun getTextShadowColor ()I
public fun getTextShadowOffsetDx ()F
public fun getTextShadowOffsetDy ()F
public fun getTextShadowRadius ()F
public fun isBackgroundColorSet ()Z
public fun isColorSet ()Z
public fun isLineThroughTextDecorationSet ()Z
public fun isUnderlineTextDecorationSet ()Z
public fun setAccessibilityRole (Ljava/lang/String;)V
public fun setAdjustFontSizeToFit (Z)V
public fun setAllowFontScaling (Z)V
@@ -7084,12 +7100,19 @@ public class com/facebook/react/views/text/ReactVirtualTextViewManager$$PropsSet
public fun setProperty (Lcom/facebook/react/views/text/ReactVirtualTextViewManager;Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V
}
public class com/facebook/react/views/text/SetSpanOperation {
protected field end I
protected field start I
protected field what Lcom/facebook/react/views/text/ReactSpan;
public fun execute (Landroid/text/SpannableStringBuilder;I)V
}
public class com/facebook/react/views/text/ShadowStyleSpan : android/text/style/CharacterStyle, com/facebook/react/views/text/ReactSpan {
public fun <init> (FFFI)V
public fun updateDrawState (Landroid/text/TextPaint;)V
}
public class com/facebook/react/views/text/TextAttributeProps {
public class com/facebook/react/views/text/TextAttributeProps : com/facebook/react/views/text/EffectiveTextAttributeProvider {
public static final field TA_KEY_ACCESSIBILITY_ROLE S
public static final field TA_KEY_ALIGNMENT S
public static final field TA_KEY_ALLOW_FONT_SCALING S
@@ -7117,7 +7140,6 @@ public class com/facebook/react/views/text/TextAttributeProps {
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY S
public static final field TA_KEY_TEXT_SHADOW_RADIUS S
public static final field TA_KEY_TEXT_TRANSFORM S
public static final field UNSET I
protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
protected field mAllowFontScaling Z
protected field mBackgroundColor I
@@ -7149,13 +7171,32 @@ public class com/facebook/react/views/text/TextAttributeProps {
protected field mTextTransform Lcom/facebook/react/views/text/TextTransform;
public static fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps;
public static fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps;
public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
public fun getBackgroundColor ()I
public fun getColor ()I
public fun getEffectiveFontSize ()I
public fun getEffectiveLetterSpacing ()F
public fun getEffectiveLineHeight ()F
public fun getFontFamily ()Ljava/lang/String;
public fun getFontFeatureSettings ()Ljava/lang/String;
public fun getFontStyle ()I
public fun getFontWeight ()I
public static fun getHyphenationFrequency (Ljava/lang/String;)I
public static fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I
public static fun getLayoutDirection (Ljava/lang/String;)I
public fun getLetterSpacing ()F
public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
public static fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I
public static fun getTextBreakStrategy (Ljava/lang/String;)I
public fun getTextShadowColor ()I
public fun getTextShadowOffsetDx ()F
public fun getTextShadowOffsetDy ()F
public fun getTextShadowRadius ()F
public fun getTextTransform ()Lcom/facebook/react/views/text/TextTransform;
public fun isBackgroundColorSet ()Z
public fun isColorSet ()Z
public fun isLineThroughTextDecorationSet ()Z
public fun isUnderlineTextDecorationSet ()Z
}
public class com/facebook/react/views/text/TextAttributes {
@@ -10,4 +10,12 @@ package com.facebook.react.common;
public class ReactConstants {
public static final String TAG = "ReactNative";
/**
* 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".
*/
public static final int UNSET = -1;
}
@@ -15,6 +15,7 @@ import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.common.ReactConstants;
import java.util.HashMap;
import java.util.Map;
@@ -167,8 +168,6 @@ public class ReactFontManager {
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 +176,11 @@ public class ReactFontManager {
public TypefaceStyle(int weight, boolean italic) {
mItalic = italic;
mWeight = weight == UNSET ? NORMAL : weight;
mWeight = weight == ReactConstants.UNSET ? NORMAL : weight;
}
public TypefaceStyle(int style) {
if (style == UNSET) {
if (style == ReactConstants.UNSET) {
style = Typeface.NORMAL;
}
@@ -194,12 +193,13 @@ public class ReactFontManager {
* existing weight bit in `style` will be used.
*/
public TypefaceStyle(int style, int weight) {
if (style == UNSET) {
if (style == ReactConstants.UNSET) {
style = Typeface.NORMAL;
}
mItalic = (style & Typeface.ITALIC) != 0;
mWeight = weight == UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight;
mWeight =
weight == ReactConstants.UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight;
}
public int getNearestStyle() {
@@ -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;
}
@@ -0,0 +1,48 @@
/*
* 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 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
}
@@ -14,6 +14,7 @@ import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.assets.ReactFontManager;
@Nullsafe(Nullsafe.Mode.LOCAL)
@@ -61,13 +62,11 @@ public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan {
}
public int getStyle() {
return mStyle == ReactFontManager.TypefaceStyle.UNSET ? Typeface.NORMAL : mStyle;
return mStyle == ReactConstants.UNSET ? Typeface.NORMAL : mStyle;
}
public int getWeight() {
return mWeight == ReactFontManager.TypefaceStyle.UNSET
? ReactFontManager.TypefaceStyle.NORMAL
: mWeight;
return mWeight == ReactConstants.UNSET ? ReactFontManager.TypefaceStyle.NORMAL : mWeight;
}
public @Nullable String getFontFamily() {
@@ -0,0 +1,22 @@
/*
* 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 com.facebook.react.common.ReactConstants.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
}
@@ -0,0 +1,63 @@
/*
* 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 com.facebook.react.common.ReactConstants
/**
* 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 {
ReactConstants.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
}
}
}
@@ -21,7 +21,7 @@ import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
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 +49,13 @@ import java.util.Map;
* <p>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";
@@ -77,6 +77,23 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
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) {
@@ -93,7 +110,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
TextTransform.apply(
((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform()));
} else if (child instanceof ReactBaseTextShadowNode) {
buildSpannedFromShadowNode(
buildSpannedFromShadowNodeDuplicated(
(ReactBaseTextShadowNode) child,
sb,
ops,
@@ -177,8 +194,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
}
if (textShadowNode.mFontStyle != UNSET
|| textShadowNode.mFontWeight != UNSET
if (textShadowNode.mFontStyle != ReactConstants.UNSET
|| textShadowNode.mFontWeight != ReactConstants.UNSET
|| textShadowNode.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -221,6 +238,99 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
private static void buildSpannedFromShadowNodeUnified(
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;
}
final HierarchicTextAttributeProvider 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 <Text> or <TextInput> 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<SetSpanOperation> ops, SpannableStringBuilder sb, ReactTextInlineImageShadowNode child) {
// We make the image take up 1 character in the span and put a corresponding character into
// the text so that the image doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
child.buildInlineImageSpan()));
}
private static void addInlineViewPlaceholderSpan(
List<SetSpanOperation> ops, SpannableStringBuilder sb, ReactShadowNode child) {
YogaValue widthValue = child.getStyleWidth();
YogaValue heightValue = child.getStyleHeight();
float width;
float height;
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
// If the measurement of the child isn't calculated, we calculate the layout for the
// view using Yoga
child.calculateLayout();
width = child.getLayoutWidth();
height = child.getLayoutHeight();
} else {
width = widthValue.value;
height = heightValue.value;
}
// We make the inline view take up 1 character in the span and put a corresponding character
// into the text so that
// the inline view doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
TextLayoutUtils.addInlineViewPlaceholderSpan(ops, sb, child.getReactTag(), width, height);
}
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
protected Spannable spannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode,
@@ -307,7 +417,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
protected @Nullable AccessibilityRole mAccessibilityRole = null;
protected @Nullable Role mRole = null;
protected int mNumberOfLines = UNSET;
protected int mNumberOfLines = ReactConstants.UNSET;
protected int mTextAlign = Gravity.NO_GRAVITY;
protected int mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
protected int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
@@ -329,9 +439,9 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
* 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 = ReactConstants.UNSET;
protected int mFontWeight = UNSET;
protected int mFontWeight = ReactConstants.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 +494,9 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
return textAlign;
}
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = UNSET)
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET)
public void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines;
markUpdated();
}
@@ -452,6 +562,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
markUpdated();
}
@Override
public int getColor() {
return mColor;
}
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
@@ -461,6 +576,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
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 +601,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@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 +619,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@Override
public @Nullable Role getRole() {
return mRole;
}
@ReactProp(name = ViewProps.ROLE)
public void setRole(@Nullable String role) {
if (isVirtual()) {
@@ -492,12 +632,22 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@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 +667,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@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 +707,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
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 +754,21 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
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 +777,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
@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) {
@@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactShadowNode;
@@ -80,7 +81,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
int minimumFontSize =
(int) Math.max(mMinimumFontScale * initialFontSize, PixelUtil.toPixelFromDIP(4));
while (currentFontSize > minimumFontSize
&& (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines
&& (mNumberOfLines != ReactConstants.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 class ReactTextShadowNode extends ReactBaseTextShadowNode {
}
final int lineCount =
mNumberOfLines == UNSET
mNumberOfLines == ReactConstants.UNSET
? layout.getLineCount()
: Math.min(mNumberOfLines, layout.getLineCount());
@@ -327,7 +328,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
mPreparedSpannableText,
UNSET,
ReactConstants.UNSET,
mContainsImages,
getPadding(Spacing.START),
getPadding(Spacing.TOP),
@@ -7,10 +7,9 @@
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.ReactConstants;
/**
* Class that contains the data needed for a text update. Used by both <Text/> and <TextInput/>
@@ -67,10 +66,10 @@ public class ReactTextUpdate {
text,
jsEventCounter,
containsImages,
UNSET,
UNSET,
UNSET,
UNSET,
ReactConstants.UNSET,
ReactConstants.UNSET,
ReactConstants.UNSET,
ReactConstants.UNSET,
textAlign,
textBreakStrategy,
justificationMode);
@@ -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;
@@ -376,10 +374,10 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
// 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 != ReactConstants.UNSET
&& paddingTop != ReactConstants.UNSET
&& paddingRight != ReactConstants.UNSET
&& paddingBottom != ReactConstants.UNSET) {
setPadding(
(int) Math.floor(paddingLeft),
@@ -13,6 +13,7 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.assets.ReactFontManager;
import java.util.ArrayList;
import java.util.List;
@@ -45,7 +46,7 @@ public class ReactTypefaceUtils {
return 900;
}
}
return ReactFontManager.TypefaceStyle.UNSET;
return ReactConstants.UNSET;
}
public static int parseFontStyle(@Nullable String fontStyleString) {
@@ -57,7 +58,7 @@ public class ReactTypefaceUtils {
return Typeface.NORMAL;
}
}
return ReactFontManager.TypefaceStyle.UNSET;
return ReactConstants.UNSET;
}
public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) {
@@ -12,7 +12,7 @@ import android.text.SpannableStringBuilder;
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;
@@ -12,6 +12,7 @@ import android.text.Layout;
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;
@@ -30,7 +31,7 @@ import java.util.List;
// 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 +62,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,17 +83,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 = ReactConstants.UNSET;
protected int mFontSize = ReactConstants.UNSET;
protected float mFontSizeInput = ReactConstants.UNSET;
protected float mLineHeightInput = ReactConstants.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;
// `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol)
protected int mLayoutDirection = ReactConstants.UNSET;
protected TextTransform mTextTransform = TextTransform.NONE;
@NonNull protected TextTransform mTextTransform = TextTransform.NONE;
protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
@@ -108,8 +107,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 = ReactConstants.UNSET;
protected int mFontWeight = ReactConstants.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 +232,11 @@ public class TextAttributeProps {
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, ReactConstants.UNSET));
result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.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, ReactConstants.UNSET));
result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
result.setColor(
props.hasKey(ViewProps.FOREGROUND_COLOR)
@@ -343,6 +342,7 @@ public class TextAttributeProps {
// 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 +352,12 @@ public class TextAttributeProps {
}
private void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines;
}
private void setLineHeight(float lineHeight) {
mLineHeightInput = lineHeight;
if (lineHeight == UNSET) {
if (lineHeight == ReactConstants.UNSET) {
mLineHeight = Float.NaN;
} else {
mLineHeight =
@@ -371,6 +371,12 @@ public class TextAttributeProps {
mLetterSpacingInput = letterSpacing;
}
@Override
@NonNull
public TextTransform getTextTransform() {
return mTextTransform;
}
public float getLetterSpacing() {
float letterSpacingPixels =
mAllowFontScaling
@@ -386,6 +392,16 @@ public class TextAttributeProps {
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 +413,7 @@ public class TextAttributeProps {
private void setFontSize(float fontSize) {
mFontSizeInput = fontSize;
if (fontSize != UNSET) {
if (fontSize != ReactConstants.UNSET) {
fontSize =
mAllowFontScaling
? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
@@ -406,6 +422,11 @@ public class TextAttributeProps {
mFontSize = (int) fontSize;
}
@Override
public int getColor() {
return mColor;
}
private void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
if (mIsColorSet) {
@@ -413,6 +434,16 @@ public class TextAttributeProps {
}
}
@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 +455,21 @@ public class TextAttributeProps {
// }
}
@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 +572,16 @@ public class TextAttributeProps {
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 +608,16 @@ public class TextAttributeProps {
}
}
@Override
public boolean isUnderlineTextDecorationSet() {
return mIsUnderlineTextDecorationSet;
}
@Override
public boolean isLineThroughTextDecorationSet() {
return mIsLineThroughTextDecorationSet;
}
private void setTextShadowOffset(ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
mTextShadowOffsetDy = 0;
@@ -570,10 +636,20 @@ public class TextAttributeProps {
}
}
@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 +657,14 @@ public class TextAttributeProps {
public static int getLayoutDirection(@Nullable String layoutDirection) {
int androidLayoutDirection;
if (layoutDirection == null || "undefined".equals(layoutDirection)) {
androidLayoutDirection = UNSET;
androidLayoutDirection = ReactConstants.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 = ReactConstants.UNSET;
}
return androidLayoutDirection;
}
@@ -597,12 +673,22 @@ public class TextAttributeProps {
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 +710,11 @@ public class TextAttributeProps {
}
}
@Override
public AccessibilityRole getAccessibilityRole() {
return mAccessibilityRole;
}
private void setAccessibilityRole(@Nullable String accessibilityRole) {
if (accessibilityRole == null) {
mAccessibilityRole = null;
@@ -632,6 +723,12 @@ public class TextAttributeProps {
}
}
@Nullable
@Override
public Role getRole() {
return mRole;
}
private void setRole(@Nullable String role) {
if (role == null) {
mRole = null;
@@ -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,7 +30,8 @@ public class TextAttributes {
private float mLetterSpacing = Float.NaN;
private float mMaxFontSizeMultiplier = Float.NaN;
private float mHeightOfTallestInlineViewOrImage = Float.NaN;
private TextTransform mTextTransform = TextTransform.UNSET;
@NonNull private TextTransform mTextTransform = TextTransform.UNSET;
public TextAttributes() {}
@@ -118,7 +120,7 @@ public class TextAttributes {
return mTextTransform;
}
public void setTextTransform(TextTransform textTransform) {
public void setTextTransform(@NonNull TextTransform textTransform) {
mTextTransform = textTransform;
}
@@ -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.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.ReactConstants;
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 class TextLayoutManager {
sTagToSpannableCache.remove(reactTag);
}
private static void buildSpannableFromFragment(
private static void buildSpannableFromFragments(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> 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 @@ public class TextLayoutManager {
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
if (textAttributes.mFontStyle != ReactConstants.UNSET
|| textAttributes.mFontWeight != ReactConstants.UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -197,6 +210,17 @@ public class TextLayoutManager {
}
}
private static void buildSpannableFromFragmentsUnified(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
final BridgeTextFragmentList 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 @@ public class TextLayoutManager {
// a new spannable will be wiped out
List<SetSpanOperation> 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 class TextLayoutManager {
int maximumNumberOfLines =
paragraphAttributes.hasKey(MAXIMUM_NUMBER_OF_LINES_KEY)
? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY)
: UNSET;
: ReactConstants.UNSET;
int calculatedLineCount =
maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
: Math.min(maximumNumberOfLines, layout.getLineCount());
@@ -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.common.logging.FLog;
import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.ReactConstants;
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 class TextLayoutManagerMapBuffer {
== LayoutDirection.RTL;
}
private static void buildSpannableFromFragment(
private static void buildSpannableFromFragments(
Context context, MapBuffer fragments, SpannableStringBuilder sb, List<SetSpanOperation> 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<SetSpanOperation> ops) {
for (int i = 0, length = fragments.getCount(); i < length; i++) {
@@ -172,8 +183,8 @@ public class TextLayoutManagerMapBuffer {
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
if (textAttributes.mFontStyle != ReactConstants.UNSET
|| textAttributes.mFontWeight != ReactConstants.UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
@@ -217,6 +228,14 @@ public class TextLayoutManagerMapBuffer {
}
}
private static void buildSpannableFromFragmentsUnified(
Context context, MapBuffer fragments, SpannableStringBuilder sb, List<SetSpanOperation> ops) {
final MapBufferTextFragmentList 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 @@ public class TextLayoutManagerMapBuffer {
// a new spannable will be wiped out
List<SetSpanOperation> 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 class TextLayoutManagerMapBuffer {
int maximumNumberOfLines =
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
: UNSET;
: ReactConstants.UNSET;
int calculatedLineCount =
maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
: Math.min(maximumNumberOfLines, layout.getLineCount());
@@ -0,0 +1,280 @@
/*
* 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.ReactConstants
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<SetSpanOperation>,
) {
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<SetSpanOperation>,
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<SetSpanOperation>,
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<SetSpanOperation>,
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<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
start: Int,
end: Int
) {
if (textAttributeProvider.isColorSet) {
ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textAttributeProvider.color)))
}
}
@JvmStatic
private fun addBackgroundColorSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
start: Int,
end: Int
) {
if (textAttributeProvider.isBackgroundColorSet) {
ops.add(
SetSpanOperation(
start, end, ReactBackgroundColorSpan(textAttributeProvider.backgroundColor)))
}
}
@JvmStatic
private fun addLetterSpacingSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
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<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
start: Int,
end: Int
) {
val effectiveFontSize = textAttributeProvider.effectiveFontSize
if (effectiveFontSize != ReactConstants.UNSET) {
ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize)))
}
}
@JvmStatic
private fun addCustomStyleSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
context: Context,
start: Int,
end: Int
) {
val fontStyle = textAttributeProvider.fontStyle
val fontWeight = textAttributeProvider.fontWeight
val fontFamily = textAttributeProvider.fontFamily
if (fontStyle != ReactConstants.UNSET ||
fontWeight != ReactConstants.UNSET ||
fontFamily != null) {
ops.add(
SetSpanOperation(
start,
end,
CustomStyleSpan(
fontStyle,
fontWeight,
textAttributeProvider.fontFeatureSettings,
fontFamily,
context.assets)))
}
}
@JvmStatic
private fun addUnderlineSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
start: Int,
end: Int
) {
if (textAttributeProvider.isUnderlineTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactUnderlineSpan()))
}
}
@JvmStatic
private fun addStrikethroughSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
textAttributeProvider: EffectiveTextAttributeProvider,
start: Int,
end: Int
) {
if (textAttributeProvider.isLineThroughTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan()))
}
}
@JvmStatic
private fun addShadowStyleSpanIfApplicable(
ops: MutableList<SetSpanOperation>,
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<SetSpanOperation>,
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<SetSpanOperation>,
start: Int,
end: Int,
reactTag: Int
) {
ops.add(SetSpanOperation(start, end, ReactTagSpan(reactTag)))
}
}
@@ -7,6 +7,7 @@
package com.facebook.react.views.text;
import androidx.annotation.Nullable;
import java.text.BreakIterator;
/** Types of text transforms for CustomTextTransformSpan */
@@ -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;
}
@@ -0,0 +1,39 @@
/*
* 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.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)
}
@@ -0,0 +1,18 @@
/*
* 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.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()
}
@@ -0,0 +1,42 @@
/*
* 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.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())
}
@@ -0,0 +1,19 @@
/*
* 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.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
}
@@ -0,0 +1,29 @@
/*
* 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.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
}
@@ -0,0 +1,15 @@
/*
* 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.fragments
/** Interface for a list of [TextFragment]s */
internal interface TextFragmentList {
fun getFragment(index: Int): TextFragment
val count: Int
}
@@ -46,6 +46,7 @@ import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.common.ReactConstants;
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<TextWatcher> 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 = ReactConstants.UNSET;
private int mFontStyle = ReactConstants.UNSET;
private boolean mAutoFocus = false;
private boolean mDidAttachToWindow = false;
private @Nullable String mPlaceholder = null;
@@ -379,7 +378,7 @@ public class ReactEditText extends AppCompatEditText {
return;
}
if (start != UNSET && end != UNSET) {
if (start != ReactConstants.UNSET && end != ReactConstants.UNSET) {
// clamp selection values for safety
start = clampToTextLength(start);
end = clampToTextLength(end);
@@ -588,8 +587,8 @@ public class ReactEditText extends AppCompatEditText {
// Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
// nonstandard
if (mFontStyle != UNSET
|| mFontWeight != UNSET
if (mFontStyle != ReactConstants.UNSET
|| mFontWeight != ReactConstants.UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
@@ -819,8 +818,8 @@ public class ReactEditText extends AppCompatEditText {
new CustomLetterSpacingSpan(effectiveLetterSpacing), 0, workingText.length(), spanFlags);
}
if (mFontStyle != UNSET
|| mFontWeight != UNSET
if (mFontStyle != ReactConstants.UNSET
|| mFontWeight != ReactConstants.UNSET
|| mFontFamily != null
|| getFontFeatureSettings() != null) {
workingText.setSpan(
@@ -36,7 +36,7 @@ import com.facebook.yoga.YogaNode;
public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
implements YogaMeasureFunction {
private int mMostRecentEventCount = UNSET;
private int mMostRecentEventCount = ReactConstants.UNSET;
private @Nullable EditText mInternalEditText;
private @Nullable ReactTextInputLocalData mLocalData;
@@ -108,7 +108,7 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
} else {
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.getEffectiveFontSize());
if (mNumberOfLines != UNSET) {
if (mNumberOfLines != ReactConstants.UNSET) {
editText.setLines(mNumberOfLines);
}
@@ -191,7 +191,7 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
super.onCollectExtraUpdates(uiViewOperationQueue);
if (mMostRecentEventCount != UNSET) {
if (mMostRecentEventCount != ReactConstants.UNSET) {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
spannedFromShadowNode(