Migrate ReactBaseTextShadowNode to Kotlin (#52449)

Summary:
Migrate com.facebook.react.views.text.ReactBaseTextShadowNode to Kotlin.

## Changelog:

[Android][Changed] - Migrated ReactBaseTextShadowNode to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation.

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

Test Plan:
```bash
yarn test-android
yarn android
```

Reviewed By: mdvacca

Differential Revision: D79341403

Pulled By: cortinico

fbshipit-source-id: ff5dd7a8c3e0220812dd3a214d8d680ccd83f4d8
This commit is contained in:
Mateo Guzmán
2025-08-08 10:36:32 -07:00
committed by Facebook GitHub Bot
parent 0aeb26bdfc
commit 8ccfff9a46
5 changed files with 771 additions and 762 deletions
@@ -6012,6 +6012,7 @@ public final class com/facebook/react/views/text/DefaultStyleValuesUtil {
}
public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode {
public static final field Companion Lcom/facebook/react/views/text/ReactBaseTextShadowNode$Companion;
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;
@@ -6019,61 +6020,86 @@ 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;
protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
protected field mAdjustsFontSizeToFit Z
protected field mBackgroundColor I
protected field mColor I
protected field mContainsImages Z
protected field mFontFamily Ljava/lang/String;
protected field mFontFeatureSettings Ljava/lang/String;
protected field mFontStyle I
protected field mFontWeight I
protected field mHyphenationFrequency I
protected field mIncludeFontPadding Z
protected field mInlineViews Ljava/util/Map;
protected field mIsBackgroundColorSet Z
protected field mIsColorSet Z
protected field mIsLineThroughTextDecorationSet Z
protected field mIsUnderlineTextDecorationSet Z
protected field mJustificationMode I
protected field mMinimumFontScale F
protected field mNumberOfLines I
protected field mReactTextViewManagerCallback Lcom/facebook/react/views/text/ReactTextViewManagerCallback;
protected field mRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
protected field mTextAlign I
protected field mTextAttributes Lcom/facebook/react/views/text/TextAttributes;
protected field mTextBreakStrategy I
protected field mTextShadowColor I
protected field mTextShadowOffsetDx F
protected field mTextShadowOffsetDy F
protected field mTextShadowRadius F
public fun <init> ()V
public fun <init> (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
public fun setAccessibilityRole (Ljava/lang/String;)V
public fun setAdjustFontSizeToFit (Z)V
public fun setAllowFontScaling (Z)V
public fun setBackgroundColor (Ljava/lang/Integer;)V
public fun setColor (Ljava/lang/Integer;)V
public fun setFontFamily (Ljava/lang/String;)V
public fun setFontSize (F)V
public fun setFontStyle (Ljava/lang/String;)V
public fun setFontVariant (Lcom/facebook/react/bridge/ReadableArray;)V
public fun setFontWeight (Ljava/lang/String;)V
public fun setIncludeFontPadding (Z)V
public fun setLetterSpacing (F)V
public fun setLineHeight (F)V
public fun setMaxFontSizeMultiplier (F)V
public fun setMinimumFontScale (F)V
public fun setNumberOfLines (I)V
public fun setRole (Ljava/lang/String;)V
public fun setTextAlign (Ljava/lang/String;)V
public synthetic fun <init> (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected final fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
protected final fun getAdjustsFontSizeToFit ()Z
protected final fun getBackgroundColor ()I
protected final fun getColor ()I
protected final fun getContainsImages ()Z
protected final fun getFontFamily ()Ljava/lang/String;
protected final fun getFontFeatureSettings ()Ljava/lang/String;
protected final fun getFontStyle ()I
protected final fun getFontWeight ()I
protected final fun getHyphenationFrequency ()I
protected final fun getIncludeFontPadding ()Z
protected final fun getInlineViews ()Ljava/util/Map;
protected final fun getJustificationMode ()I
protected final fun getMinimumFontScale ()F
protected final fun getNumberOfLines ()I
protected final fun getReactTextViewManagerCallback ()Lcom/facebook/react/views/text/ReactTextViewManagerCallback;
protected final fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
protected final fun getTextAlign ()I
protected final fun getTextAttributes ()Lcom/facebook/react/views/text/TextAttributes;
protected final fun getTextBreakStrategy ()I
protected final fun getTextShadowColor ()I
protected final fun getTextShadowOffsetDx ()F
protected final fun getTextShadowOffsetDy ()F
protected final fun getTextShadowRadius ()F
protected final fun isBackgroundColorSet ()Z
protected final fun isColorSet ()Z
protected final fun isLineThroughTextDecorationSet ()Z
protected final fun isUnderlineTextDecorationSet ()Z
protected final fun setAccessibilityRole (Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;)V
public final fun setAccessibilityRole (Ljava/lang/String;)V
public final fun setAdjustFontSizeToFit (Z)V
protected final fun setAdjustsFontSizeToFit (Z)V
public final fun setAllowFontScaling (Z)V
protected final fun setBackgroundColor (I)V
public final fun setBackgroundColor (Ljava/lang/Integer;)V
protected final fun setBackgroundColorSet (Z)V
protected final fun setColor (I)V
public final fun setColor (Ljava/lang/Integer;)V
protected final fun setColorSet (Z)V
protected final fun setContainsImages (Z)V
public final fun setFontFamily (Ljava/lang/String;)V
protected final fun setFontFeatureSettings (Ljava/lang/String;)V
public final fun setFontSize (F)V
protected final fun setFontStyle (I)V
public final fun setFontStyle (Ljava/lang/String;)V
public final fun setFontVariant (Lcom/facebook/react/bridge/ReadableArray;)V
protected final fun setFontWeight (I)V
public final fun setFontWeight (Ljava/lang/String;)V
protected final fun setHyphenationFrequency (I)V
public final fun setIncludeFontPadding (Z)V
protected final fun setInlineViews (Ljava/util/Map;)V
protected final fun setJustificationMode (I)V
public final fun setLetterSpacing (F)V
public final fun setLineHeight (F)V
protected final fun setLineThroughTextDecorationSet (Z)V
public final fun setMaxFontSizeMultiplier (F)V
public final fun setMinimumFontScale (F)V
public final fun setNumberOfLines (I)V
protected final fun setReactTextViewManagerCallback (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
protected final fun setRole (Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;)V
public final fun setRole (Ljava/lang/String;)V
public final fun setTextAlign (Ljava/lang/String;)V
protected final fun setTextAttributes (Lcom/facebook/react/views/text/TextAttributes;)V
protected final fun setTextBreakStrategy (I)V
public fun setTextBreakStrategy (Ljava/lang/String;)V
public fun setTextDecorationLine (Ljava/lang/String;)V
public fun setTextShadowColor (I)V
public fun setTextShadowOffset (Lcom/facebook/react/bridge/ReadableMap;)V
public fun setTextShadowRadius (F)V
public fun setTextTransform (Ljava/lang/String;)V
protected fun spannedFromShadowNode (Lcom/facebook/react/views/text/ReactBaseTextShadowNode;Ljava/lang/String;ZLcom/facebook/react/uimanager/NativeViewHierarchyOptimizer;)Landroid/text/Spannable;
public final fun setTextDecorationLine (Ljava/lang/String;)V
public final fun setTextShadowColor (I)V
public final fun setTextShadowOffset (Lcom/facebook/react/bridge/ReadableMap;)V
protected final fun setTextShadowOffsetDx (F)V
protected final fun setTextShadowOffsetDy (F)V
public final fun setTextShadowRadius (F)V
public final fun setTextTransform (Ljava/lang/String;)V
protected final fun setUnderlineTextDecorationSet (Z)V
protected final fun spannedFromShadowNode (Lcom/facebook/react/views/text/ReactBaseTextShadowNode;Ljava/lang/String;ZLcom/facebook/react/uimanager/NativeViewHierarchyOptimizer;)Landroid/text/Spannable;
}
public final class com/facebook/react/views/text/ReactBaseTextShadowNode$Companion {
}
public final class com/facebook/react/views/text/ReactFontManager {
@@ -1,669 +0,0 @@
/*
* 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.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.Gravity;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
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.ReactShadowNode;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.internal.ReactTextInlineImageShadowNode;
import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan;
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
import com.facebook.react.views.text.internal.span.CustomStyleSpan;
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan;
import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan;
import com.facebook.react.views.text.internal.span.ReactClickableSpan;
import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan;
import com.facebook.react.views.text.internal.span.ReactSpan;
import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan;
import com.facebook.react.views.text.internal.span.ReactTagSpan;
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan;
import com.facebook.react.views.text.internal.span.SetSpanOperation;
import com.facebook.react.views.text.internal.span.ShadowStyleSpan;
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
import com.facebook.yoga.YogaDirection;
import com.facebook.yoga.YogaUnit;
import com.facebook.yoga.YogaValue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* {@link ReactShadowNode} abstract class for spannable text nodes.
*
* <p>This class handles all text attributes associated with {@code <Text>}-ish node. A concrete
* node can be an anchor {@code <Text>} node, an anchor {@code <TextInput>} node or virtual {@code
* <Text>} node inside {@code <Text>} or {@code <TextInput>} node. Or even something else.
*
* <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.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
@Nullsafe(Nullsafe.Mode.LOCAL)
public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
// 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 String PROP_SHADOW_OFFSET = "textShadowOffset";
public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
public static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
public static final String PROP_SHADOW_COLOR = "textShadowColor";
public static final String PROP_TEXT_TRANSFORM = "textTransform";
public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
private static void buildSpannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode,
SpannableStringBuilder sb,
List<SetSpanOperation> ops,
@Nullable TextAttributes parentTextAttributes,
boolean supportsInlineViews,
@Nullable Map<Integer, ReactShadowNode> inlineViews,
int start) {
TextAttributes textAttributes;
if (parentTextAttributes != null) {
textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes);
} else {
textAttributes = textShadowNode.mTextAttributes;
}
for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
ReactShadowNode child = textShadowNode.getChildAt(i);
if (child instanceof ReactRawTextShadowNode) {
String childText = ((ReactRawTextShadowNode) child).getText();
if (childText != null) {
sb.append(TextTransform.apply(childText, textAttributes.textTransform));
}
} else if (child instanceof ReactBaseTextShadowNode) {
buildSpannedFromShadowNode(
(ReactBaseTextShadowNode) child,
sb,
ops,
textAttributes,
supportsInlineViews,
inlineViews,
sb.length());
} else if (child instanceof ReactTextInlineImageShadowNode) {
// We make the image take up 1 character in the span and put a corresponding character into
// the text so that the image doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
} else if (supportsInlineViews) {
int reactTag = child.getReactTag();
YogaValue widthValue = child.getStyleWidth();
YogaValue heightValue = child.getStyleHeight();
float width;
float height;
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
// If the measurement of the child isn't calculated, we calculate the layout for the
// view using Yoga
child.calculateLayout();
width = child.getLayoutWidth();
height = child.getLayoutHeight();
} else {
width = widthValue.value;
height = heightValue.value;
}
// We make the inline view take up 1 character in the span and put a corresponding character
// into
// the text so that the inline view doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER);
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
// supportsInlineViews is true, so we can assume that inlineViews is not null
Assertions.assertNotNull(inlineViews).put(reactTag, child);
} else {
throw new IllegalViewOperationException(
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
}
child.markUpdateSeen();
}
int end = sb.length();
if (end >= start) {
if (textShadowNode.mIsColorSet) {
ops.add(
new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor)));
}
if (textShadowNode.mIsBackgroundColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor)));
}
boolean roleIsLink =
textShadowNode.mRole != null
? textShadowNode.mRole == Role.LINK
: textShadowNode.mAccessibilityRole == AccessibilityRole.LINK;
if (roleIsLink) {
ops.add(
new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag())));
}
float effectiveLetterSpacing = textAttributes.getEffectiveLetterSpacing();
if (!Float.isNaN(effectiveLetterSpacing)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLetterSpacing() != effectiveLetterSpacing)) {
ops.add(
new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing)));
}
int effectiveFontSize = textAttributes.getEffectiveFontSize();
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything like
// `Float.NaN`.
parentTextAttributes == null
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
}
if (textShadowNode.mFontStyle != ReactConstants.UNSET
|| textShadowNode.mFontWeight != ReactConstants.UNSET
|| textShadowNode.mFontFamily != null) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomStyleSpan(
textShadowNode.mFontStyle,
textShadowNode.mFontWeight,
textShadowNode.mFontFeatureSettings,
textShadowNode.mFontFamily,
textShadowNode.getThemedContext().getAssets())));
}
if (textShadowNode.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
}
if (textShadowNode.mIsLineThroughTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
}
if ((textShadowNode.mTextShadowOffsetDx != 0
|| textShadowNode.mTextShadowOffsetDy != 0
|| textShadowNode.mTextShadowRadius != 0)
&& Color.alpha(textShadowNode.mTextShadowColor) != 0) {
ops.add(
new SetSpanOperation(
start,
end,
new ShadowStyleSpan(
textShadowNode.mTextShadowOffsetDx,
textShadowNode.mTextShadowOffsetDy,
textShadowNode.mTextShadowRadius,
textShadowNode.mTextShadowColor)));
}
float effectiveLineHeight = textAttributes.getEffectiveLineHeight();
if (!Float.isNaN(effectiveLineHeight)
&& (parentTextAttributes == null
|| parentTextAttributes.getEffectiveLineHeight() != effectiveLineHeight)) {
ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight)));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
}
}
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
protected Spannable spannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode,
@Nullable String text,
boolean supportsInlineViews,
@Nullable NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
Assertions.assertCondition(
!supportsInlineViews || nativeViewHierarchyOptimizer != null,
"nativeViewHierarchyOptimizer is required when inline views are supported");
SpannableStringBuilder sb = new SpannableStringBuilder();
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
// a new spannable will be wiped out
List<SetSpanOperation> ops = new ArrayList<>();
Map<Integer, ReactShadowNode> inlineViews = supportsInlineViews ? new HashMap<>() : null;
if (text != null) {
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
// TextInput).
sb.append(TextTransform.apply(text, textShadowNode.mTextAttributes.textTransform));
}
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0);
textShadowNode.mContainsImages = false;
textShadowNode.mInlineViews = inlineViews;
float heightOfTallestInlineViewOrImage = Float.NaN;
// While setting the Spans on the final text, we also check whether any of them are inline views
// or images.
for (int priorityIndex = 0; priorityIndex < ops.size(); priorityIndex++) {
final SetSpanOperation op = ops.get(ops.size() - priorityIndex - 1);
final ReactSpan what = op.what;
boolean isInlineImage = what instanceof TextInlineImageSpan;
if (isInlineImage || what instanceof TextInlineViewPlaceholderSpan) {
int height;
if (isInlineImage) {
height = ((TextInlineImageSpan) what).getHeight();
textShadowNode.mContainsImages = true;
} else {
TextInlineViewPlaceholderSpan placeholder = (TextInlineViewPlaceholderSpan) what;
height = placeholder.getHeight();
// Inline views cannot be layout-only because the ReactTextView needs to be able to grab
// ahold of them on the UI thread to size and position them.
ReactShadowNode childNode =
Assertions.assertNotNull(inlineViews).get(placeholder.getReactTag());
Assertions.assertNotNull(childNode);
Assertions.assertNotNull(nativeViewHierarchyOptimizer);
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode);
// The ReactTextView is responsible for laying out the inline views.
childNode.setLayoutParent(textShadowNode);
}
if (Float.isNaN(heightOfTallestInlineViewOrImage)
|| height > heightOfTallestInlineViewOrImage) {
heightOfTallestInlineViewOrImage = height;
}
}
// Actual order of calling {@code execute} does NOT matter,
// but the {@code priorityIndex} DOES matter.
op.execute(sb, priorityIndex);
}
textShadowNode.mTextAttributes.setHeightOfTallestInlineViewOrImage(
heightOfTallestInlineViewOrImage);
if (mReactTextViewManagerCallback != null) {
mReactTextViewManagerCallback.onPostProcessSpannable(sb);
}
return sb;
}
protected TextAttributes mTextAttributes;
protected boolean mIsColorSet = false;
protected int mColor;
protected boolean mIsBackgroundColorSet = false;
protected int mBackgroundColor;
protected @Nullable AccessibilityRole mAccessibilityRole = null;
protected @Nullable Role mRole = null;
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;
protected int mJustificationMode =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
protected float mTextShadowRadius = 0;
protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
protected boolean mIsUnderlineTextDecorationSet = false;
protected boolean mIsLineThroughTextDecorationSet = false;
protected boolean mIncludeFontPadding = true;
protected boolean mAdjustsFontSizeToFit = false;
protected float mMinimumFontScale = 0;
/**
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link
* Typeface#NORMAL} or {@link Typeface#BOLD}.
*/
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
* nodes. To retain that style, you have to add it to those nodes explicitly.
*
* <p>Example, Android 4.4:
*
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
* </pre>
*/
protected @Nullable String mFontFamily = null;
/**
* @see android.graphics.Paint#setFontFeatureSettings
*/
protected @Nullable String mFontFeatureSettings = null;
protected boolean mContainsImages = false;
// Only nullable iff `supportsInlineViews` is `false`.
protected @Nullable Map<Integer, ReactShadowNode> mInlineViews;
public ReactBaseTextShadowNode() {
this(null);
}
public ReactBaseTextShadowNode(
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
mTextAttributes = new TextAttributes();
mReactTextViewManagerCallback = reactTextViewManagerCallback;
}
// Return text alignment according to LTR or RTL style
private int getTextAlign() {
int textAlign = mTextAlign;
if (getLayoutDirection() == YogaDirection.RTL) {
if (textAlign == Gravity.RIGHT) {
textAlign = Gravity.LEFT;
} else if (textAlign == Gravity.LEFT) {
textAlign = Gravity.RIGHT;
}
}
return textAlign;
}
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET)
public void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines;
markUpdated();
}
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN)
public void setLineHeight(float lineHeight) {
mTextAttributes.setLineHeight(lineHeight);
markUpdated();
}
@ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = 0.f)
public void setLetterSpacing(float letterSpacing) {
mTextAttributes.setLetterSpacing(letterSpacing);
markUpdated();
}
@ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
public void setAllowFontScaling(boolean allowFontScaling) {
if (allowFontScaling != mTextAttributes.getAllowFontScaling()) {
mTextAttributes.setAllowFontScaling(allowFontScaling);
markUpdated();
}
}
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
if (maxFontSizeMultiplier != mTextAttributes.getMaxFontSizeMultiplier()) {
mTextAttributes.setMaxFontSizeMultiplier(maxFontSizeMultiplier);
markUpdated();
}
}
@ReactProp(name = ViewProps.TEXT_ALIGN)
public void setTextAlign(@Nullable String textAlign) {
if ("justify".equals(textAlign)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mJustificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD;
}
mTextAlign = Gravity.LEFT;
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
}
if (textAlign == null || "auto".equals(textAlign)) {
mTextAlign = Gravity.NO_GRAVITY;
} else if ("left".equals(textAlign)) {
mTextAlign = Gravity.LEFT;
} else if ("right".equals(textAlign)) {
mTextAlign = Gravity.RIGHT;
} else if ("center".equals(textAlign)) {
mTextAlign = Gravity.CENTER_HORIZONTAL;
} else {
FLog.w(ReactConstants.TAG, "Invalid textAlign: " + textAlign);
mTextAlign = Gravity.NO_GRAVITY;
}
}
markUpdated();
}
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
public void setFontSize(float fontSize) {
mTextAttributes.setFontSize(fontSize);
markUpdated();
}
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public void setColor(@Nullable Integer color) {
if (color != null) {
mIsColorSet = true;
mColor = color;
}
markUpdated();
}
@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
// the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual
// nodes get mapped to native views and native views get their background colors get set via
// {@link BaseViewManager}.
if (isVirtual()) {
if (color != null) {
mIsBackgroundColorSet = true;
mBackgroundColor = color;
}
markUpdated();
}
}
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
public void setAccessibilityRole(@Nullable String accessibilityRole) {
if (isVirtual()) {
mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole);
markUpdated();
}
}
@ReactProp(name = ViewProps.ROLE)
public void setRole(@Nullable String role) {
if (isVirtual()) {
mRole = Role.fromValue(role);
markUpdated();
}
}
@ReactProp(name = ViewProps.FONT_FAMILY)
public void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
markUpdated();
}
@ReactProp(name = ViewProps.FONT_WEIGHT)
public void setFontWeight(@Nullable String fontWeightString) {
int fontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString);
if (fontWeight != mFontWeight) {
mFontWeight = fontWeight;
markUpdated();
}
}
@ReactProp(name = ViewProps.FONT_VARIANT)
public void setFontVariant(@Nullable ReadableArray fontVariantArray) {
String fontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariantArray);
if (!TextUtils.equals(fontFeatureSettings, mFontFeatureSettings)) {
mFontFeatureSettings = fontFeatureSettings;
markUpdated();
}
}
@ReactProp(name = ViewProps.FONT_STYLE)
public void setFontStyle(@Nullable String fontStyleString) {
int fontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString);
if (fontStyle != mFontStyle) {
mFontStyle = fontStyle;
markUpdated();
}
}
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
public void setIncludeFontPadding(boolean includepad) {
mIncludeFontPadding = includepad;
}
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
public void setTextDecorationLine(@Nullable String textDecorationLineString) {
mIsUnderlineTextDecorationSet = false;
mIsLineThroughTextDecorationSet = false;
if (textDecorationLineString != null) {
for (String textDecorationLineSubString : textDecorationLineString.split(" ")) {
if ("underline".equals(textDecorationLineSubString)) {
mIsUnderlineTextDecorationSet = true;
} else if ("line-through".equals(textDecorationLineSubString)) {
mIsLineThroughTextDecorationSet = true;
}
}
}
markUpdated();
}
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
} else if ("simple".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
} else if ("balanced".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
} else {
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: " + textBreakStrategy);
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
}
markUpdated();
}
@ReactProp(name = PROP_SHADOW_OFFSET)
public void setTextShadowOffset(ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
mTextShadowOffsetDy = 0;
if (offsetMap != null) {
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
mTextShadowOffsetDx =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH));
}
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
mTextShadowOffsetDy =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT));
}
}
markUpdated();
}
@ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1)
public void setTextShadowRadius(float textShadowRadius) {
if (textShadowRadius != mTextShadowRadius) {
mTextShadowRadius = textShadowRadius;
markUpdated();
}
}
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
public void setTextShadowColor(int textShadowColor) {
if (textShadowColor != mTextShadowColor) {
mTextShadowColor = textShadowColor;
markUpdated();
}
}
@ReactProp(name = PROP_TEXT_TRANSFORM)
public void setTextTransform(@Nullable String textTransform) {
TextTransform textTransformEnum = TextTransform.UNSET;
if (textTransform == null) {
textTransformEnum = TextTransform.UNSET;
} else if ("none".equals(textTransform)) {
textTransformEnum = TextTransform.NONE;
} else if ("uppercase".equals(textTransform)) {
textTransformEnum = TextTransform.UPPERCASE;
} else if ("lowercase".equals(textTransform)) {
textTransformEnum = TextTransform.LOWERCASE;
} else if ("capitalize".equals(textTransform)) {
textTransformEnum = TextTransform.CAPITALIZE;
} else {
FLog.w(ReactConstants.TAG, "Invalid textTransform: " + textTransform);
}
mTextAttributes.textTransform = textTransformEnum;
markUpdated();
}
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
if (adjustsFontSizeToFit != mAdjustsFontSizeToFit) {
mAdjustsFontSizeToFit = adjustsFontSizeToFit;
markUpdated();
}
}
@ReactProp(name = ViewProps.MINIMUM_FONT_SCALE)
public void setMinimumFontScale(float minimumFontScale) {
if (minimumFontScale != mMinimumFontScale) {
mMinimumFontScale = minimumFontScale;
markUpdated();
}
}
}
@@ -0,0 +1,647 @@
/*
* 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.graphics.Color
import android.os.Build
import android.text.Layout
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.Gravity
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.LayoutShadowNode
import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP
import com.facebook.react.uimanager.ReactAccessibilityDelegate
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ReactShadowNodeImpl
import com.facebook.react.uimanager.ViewProps
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.facebook.react.views.text.TextTransform.Companion.apply
import com.facebook.react.views.text.internal.ReactTextInlineImageShadowNode
import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan
import com.facebook.react.views.text.internal.span.CustomStyleSpan
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan
import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan
import com.facebook.react.views.text.internal.span.ReactClickableSpan
import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan
import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan
import com.facebook.react.views.text.internal.span.ReactTagSpan
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan
import com.facebook.react.views.text.internal.span.SetSpanOperation
import com.facebook.react.views.text.internal.span.ShadowStyleSpan
import com.facebook.react.views.text.internal.span.TextInlineImageSpan
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaUnit
/**
* [ReactShadowNode] abstract class for spannable text nodes.
*
* This class handles all text attributes associated with `<Text>`-ish node. A concrete node can be
* an anchor `<Text>` node, an anchor `<TextInput>` node or virtual `<Text>` node inside `<Text>` or
* `<TextInput>` node. Or even something else.
*
* This also node calculates [Spannable] object based on sub nodes of the same type, which can be
* used in concrete classes to feed native views and compute layout.
*/
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
public abstract class ReactBaseTextShadowNode
@JvmOverloads
public constructor(
protected var reactTextViewManagerCallback: ReactTextViewManagerCallback? = null
) : LayoutShadowNode() {
protected var textAttributes: TextAttributes = TextAttributes()
protected var isColorSet: Boolean = false
protected var color: Int = 0
protected var isBackgroundColorSet: Boolean = false
protected var backgroundColor: Int = 0
protected var accessibilityRole: AccessibilityRole? = null
protected var role: ReactAccessibilityDelegate.Role? = null
protected var numberOfLines: Int = ReactConstants.UNSET
private set
protected var textBreakStrategy: Int = Layout.BREAK_STRATEGY_HIGH_QUALITY
protected var hyphenationFrequency: Int = Layout.HYPHENATION_FREQUENCY_NONE
protected var justificationMode: Int =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
0
} else {
Layout.JUSTIFICATION_MODE_NONE
}
// Return text alignment according to LTR or RTL style
protected var textAlign: Int = Gravity.NO_GRAVITY
get() {
return if (layoutDirection == YogaDirection.RTL) {
when (field) {
Gravity.RIGHT -> Gravity.LEFT
Gravity.LEFT -> Gravity.RIGHT
else -> field
}
} else {
field
}
}
private set
/**
* [fontStyle] can be [Typeface.NORMAL] or [Typeface.ITALIC]. [fontWeight] can be
* [Typeface.NORMAL] or [Typeface.BOLD].
*/
protected var fontStyle: Int = ReactConstants.UNSET
protected var fontWeight: Int = 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
* nodes. To retain that style, you have to add it to those nodes explicitly.
*
* Example, Android 4.4:
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
* </pre>
*/
protected var fontFamily: String? = null
private set
/** @see [android.graphics.Paint.setFontFeatureSettings] */
protected var fontFeatureSettings: String? = null
protected var includeFontPadding: Boolean = true
private set
protected var adjustsFontSizeToFit: Boolean = false
protected var minimumFontScale: Float = 0f
private set
protected var textShadowOffsetDx: Float = 0f
protected var textShadowOffsetDy: Float = 0f
protected var textShadowRadius: Float = 0f
private set
protected var textShadowColor: Int = DEFAULT_TEXT_SHADOW_COLOR
private set
protected var isUnderlineTextDecorationSet: Boolean = false
protected var isLineThroughTextDecorationSet: Boolean = false
protected var containsImages: Boolean = false
// Only nullable if `supportsInlineViews` is `false`.
protected var inlineViews: Map<Int, ReactShadowNode<*>>? = null
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
@Suppress("DEPRECATION")
protected fun spannedFromShadowNode(
textShadowNode: ReactBaseTextShadowNode,
text: String?,
supportsInlineViews: Boolean,
nativeViewHierarchyOptimizer: com.facebook.react.uimanager.NativeViewHierarchyOptimizer?
): Spannable {
check(!supportsInlineViews || nativeViewHierarchyOptimizer != null) {
"nativeViewHierarchyOptimizer is required when inline views are supported"
}
val sb = SpannableStringBuilder()
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
// The [SpannableStringBuilder] implementation require setSpan operation to be called
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
// a new spannable will be wiped out
val ops: MutableList<SetSpanOperation> = ArrayList()
val inlineViews: MutableMap<Int, ReactShadowNode<*>>? =
if (supportsInlineViews) HashMap() else null
if (text != null) {
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
// TextInput).
sb.append(apply(text, textShadowNode.textAttributes.textTransform))
}
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0)
textShadowNode.containsImages = false
textShadowNode.inlineViews = inlineViews
var heightOfTallestInlineViewOrImage = Float.NaN
// While setting the Spans on the final text, we also check whether any of them are inline views
// or images.
for (priorityIndex in ops.indices) {
val op = ops[ops.size - priorityIndex - 1]
val what = op.what
val isInlineImage = what is TextInlineImageSpan
if (isInlineImage || what is TextInlineViewPlaceholderSpan) {
val height: Int
if (isInlineImage) {
height = what.height
textShadowNode.containsImages = true
} else {
val placeholder = what as TextInlineViewPlaceholderSpan
height = placeholder.height
// Inline views cannot be layout-only because ReactTextView needs to access
// them on the UI thread to measure and position them.
val childNode = checkNotNull(inlineViews)[placeholder.reactTag]
checkNotNull(childNode)
checkNotNull(nativeViewHierarchyOptimizer)
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode)
// The ReactTextView is responsible for laying out the inline views.
@Suppress("UNCHECKED_CAST")
(childNode as ReactShadowNode<ReactShadowNodeImpl>).setLayoutParent(textShadowNode)
}
if (heightOfTallestInlineViewOrImage.isNaN() || height > heightOfTallestInlineViewOrImage) {
heightOfTallestInlineViewOrImage = height.toFloat()
}
}
// Actual order of calling `execute` does NOT matter,
// but the `priorityIndex` DOES matter.
op.execute(sb, priorityIndex)
}
textShadowNode.textAttributes.heightOfTallestInlineViewOrImage =
heightOfTallestInlineViewOrImage
reactTextViewManagerCallback?.onPostProcessSpannable(sb)
return sb
}
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET)
public fun setNumberOfLines(numberOfLines: Int) {
this.numberOfLines = if (numberOfLines == 0) ReactConstants.UNSET else numberOfLines
markUpdated()
}
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN)
public fun setLineHeight(lineHeight: Float) {
textAttributes.lineHeight = lineHeight
markUpdated()
}
@ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = 0f)
public fun setLetterSpacing(letterSpacing: Float) {
textAttributes.letterSpacing = letterSpacing
markUpdated()
}
@ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
public fun setAllowFontScaling(allowFontScaling: Boolean) {
if (allowFontScaling != textAttributes.allowFontScaling) {
textAttributes.allowFontScaling = allowFontScaling
markUpdated()
}
}
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
public fun setMaxFontSizeMultiplier(maxFontSizeMultiplier: Float) {
if (maxFontSizeMultiplier != textAttributes.maxFontSizeMultiplier) {
textAttributes.maxFontSizeMultiplier = maxFontSizeMultiplier
markUpdated()
}
}
@ReactProp(name = ViewProps.TEXT_ALIGN)
public fun setTextAlign(textAlign: String?) {
if ("justify" == textAlign) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD
}
this.textAlign = Gravity.LEFT
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
justificationMode = Layout.JUSTIFICATION_MODE_NONE
}
this.textAlign =
when (textAlign) {
null,
"auto" -> Gravity.NO_GRAVITY
"left" -> Gravity.LEFT
"right" -> Gravity.RIGHT
"center" -> Gravity.CENTER_HORIZONTAL
else -> {
FLog.w(ReactConstants.TAG, "Invalid textAlign: $textAlign")
Gravity.NO_GRAVITY
}
}
}
markUpdated()
}
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
public fun setFontSize(fontSize: Float) {
textAttributes.fontSize = fontSize
markUpdated()
}
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public fun setColor(color: Int?) {
color?.let { newColor ->
isColorSet = true
this.color = newColor
}
markUpdated()
}
@ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color")
public fun setBackgroundColor(color: Int?) {
// Background color needs to be handled here for virtual nodes so it can be incorporated into
// the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual
// nodes get mapped to native views and native views get their background colors get set via
// [BaseViewManager].
if (isVirtual) {
color?.let { newColor ->
isBackgroundColorSet = true
backgroundColor = newColor
}
markUpdated()
}
}
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
public fun setAccessibilityRole(accessibilityRole: String?) {
if (isVirtual) {
this.accessibilityRole = AccessibilityRole.fromValue(accessibilityRole)
markUpdated()
}
}
@ReactProp(name = ViewProps.ROLE)
public fun setRole(role: String?) {
if (isVirtual) {
this.role = ReactAccessibilityDelegate.Role.fromValue(role)
markUpdated()
}
}
@ReactProp(name = ViewProps.FONT_FAMILY)
public fun setFontFamily(fontFamily: String?) {
this.fontFamily = fontFamily
markUpdated()
}
@ReactProp(name = ViewProps.FONT_WEIGHT)
public fun setFontWeight(fontWeightString: String?) {
val parsedFontWeight = parseFontWeight(fontWeightString)
if (parsedFontWeight != fontWeight) {
fontWeight = parsedFontWeight
markUpdated()
}
}
@ReactProp(name = ViewProps.FONT_VARIANT)
public fun setFontVariant(fontVariantArray: ReadableArray?) {
val fontFeatureSettings = parseFontVariant(fontVariantArray)
if (fontFeatureSettings != this.fontFeatureSettings) {
this.fontFeatureSettings = fontFeatureSettings
markUpdated()
}
}
@ReactProp(name = ViewProps.FONT_STYLE)
public fun setFontStyle(fontStyleString: String?) {
val parsedFontStyle = parseFontStyle(fontStyleString)
if (parsedFontStyle != fontStyle) {
fontStyle = parsedFontStyle
markUpdated()
}
}
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
public fun setIncludeFontPadding(includepad: Boolean) {
includeFontPadding = includepad
}
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
public fun setTextDecorationLine(textDecorationLineString: String?) {
isUnderlineTextDecorationSet = false
isLineThroughTextDecorationSet = false
if (textDecorationLineString != null) {
for (textDecorationLineSubString in
textDecorationLineString.split(" ").dropLastWhile { it.isEmpty() }.toTypedArray()) {
if ("underline" == textDecorationLineSubString) {
isUnderlineTextDecorationSet = true
} else if ("line-through" == textDecorationLineSubString) {
isLineThroughTextDecorationSet = true
}
}
}
markUpdated()
}
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
public open fun setTextBreakStrategy(textBreakStrategy: String?) {
this.textBreakStrategy =
when (textBreakStrategy) {
null,
"highQuality" -> Layout.BREAK_STRATEGY_HIGH_QUALITY
"simple" -> Layout.BREAK_STRATEGY_SIMPLE
"balanced" -> Layout.BREAK_STRATEGY_BALANCED
else -> {
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: $textBreakStrategy")
Layout.BREAK_STRATEGY_HIGH_QUALITY
}
}
markUpdated()
}
@ReactProp(name = PROP_SHADOW_OFFSET)
public fun setTextShadowOffset(offsetMap: ReadableMap?) {
textShadowOffsetDx = 0f
textShadowOffsetDy = 0f
offsetMap?.let { map ->
if (map.hasKey(PROP_SHADOW_OFFSET_WIDTH) && !map.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
textShadowOffsetDx = toPixelFromDIP(map.getDouble(PROP_SHADOW_OFFSET_WIDTH))
}
if (map.hasKey(PROP_SHADOW_OFFSET_HEIGHT) && !map.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
textShadowOffsetDy = toPixelFromDIP(map.getDouble(PROP_SHADOW_OFFSET_HEIGHT))
}
}
markUpdated()
}
@ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1)
public fun setTextShadowRadius(textShadowRadius: Float) {
if (textShadowRadius != this.textShadowRadius) {
this.textShadowRadius = textShadowRadius
markUpdated()
}
}
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
public fun setTextShadowColor(textShadowColor: Int) {
if (textShadowColor != this.textShadowColor) {
this.textShadowColor = textShadowColor
markUpdated()
}
}
@ReactProp(name = PROP_TEXT_TRANSFORM)
public fun setTextTransform(textTransform: String?) {
val textTransformEnum =
when (textTransform) {
null -> TextTransform.UNSET
"none" -> TextTransform.NONE
"uppercase" -> TextTransform.UPPERCASE
"lowercase" -> TextTransform.LOWERCASE
"capitalize" -> TextTransform.CAPITALIZE
else -> {
FLog.w(ReactConstants.TAG, "Invalid textTransform: $textTransform")
TextTransform.UNSET
}
}
textAttributes.textTransform = textTransformEnum
markUpdated()
}
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
public fun setAdjustFontSizeToFit(adjustsFontSizeToFit: Boolean) {
if (adjustsFontSizeToFit != this.adjustsFontSizeToFit) {
this.adjustsFontSizeToFit = adjustsFontSizeToFit
markUpdated()
}
}
@ReactProp(name = ViewProps.MINIMUM_FONT_SCALE)
public fun setMinimumFontScale(minimumFontScale: Float) {
if (minimumFontScale != this.minimumFontScale) {
this.minimumFontScale = minimumFontScale
markUpdated()
}
}
public companion object {
// 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 const val INLINE_VIEW_PLACEHOLDER = "0"
public const val PROP_SHADOW_OFFSET: String = "textShadowOffset"
public const val PROP_SHADOW_OFFSET_WIDTH: String = "width"
public const val PROP_SHADOW_OFFSET_HEIGHT: String = "height"
public const val PROP_SHADOW_RADIUS: String = "textShadowRadius"
public const val PROP_SHADOW_COLOR: String = "textShadowColor"
public const val PROP_TEXT_TRANSFORM: String = "textTransform"
public const val DEFAULT_TEXT_SHADOW_COLOR: Int = 0x55000000
private fun buildSpannedFromShadowNode(
textShadowNode: ReactBaseTextShadowNode,
sb: SpannableStringBuilder,
ops: MutableList<SetSpanOperation>,
parentTextAttributes: TextAttributes?,
supportsInlineViews: Boolean,
inlineViews: MutableMap<Int, ReactShadowNode<*>>?,
start: Int
) {
val textAttributes =
parentTextAttributes?.applyChild(textShadowNode.textAttributes)
?: textShadowNode.textAttributes
var i = 0
val length = textShadowNode.childCount
while (i < length) {
val child: ReactShadowNode<*> = textShadowNode.getChildAt(i)
if (child is ReactRawTextShadowNode) {
child.text?.let { sb.append(apply(it, textAttributes.textTransform)) }
} else if (child is ReactBaseTextShadowNode) {
buildSpannedFromShadowNode(
child, sb, ops, textAttributes, supportsInlineViews, inlineViews, sb.length)
} else if (child is ReactTextInlineImageShadowNode) {
// We make the image take up 1 character in the span and put a corresponding character
// into the text so that the image doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER)
ops.add(
SetSpanOperation(
sb.length - INLINE_VIEW_PLACEHOLDER.length,
sb.length,
child.buildInlineImageSpan()))
} else if (supportsInlineViews) {
val reactTag = child.reactTag
val widthValue = child.styleWidth
val heightValue = child.styleHeight
val width: Float
val height: Float
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.layoutWidth
height = child.layoutHeight
} else {
width = widthValue.value
height = heightValue.value
}
// We make the inline view take up 1 character in the span and put a corresponding
// character
// into
// the text so that the inline view doesn't run over any following text.
sb.append(INLINE_VIEW_PLACEHOLDER)
ops.add(
SetSpanOperation(
sb.length - INLINE_VIEW_PLACEHOLDER.length,
sb.length,
TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt())))
// supportsInlineViews is true, so we can assume that inlineViews is not null
checkNotNull(inlineViews)[reactTag] = child
} else {
throw IllegalViewOperationException(
"Unexpected view type nested under a <Text> or <TextInput> node: ${child.javaClass}")
}
child.markUpdateSeen()
i++
}
val end = sb.length
if (end >= start) {
if (textShadowNode.isColorSet) {
ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textShadowNode.color)))
}
if (textShadowNode.isBackgroundColorSet) {
ops.add(
SetSpanOperation(
start, end, ReactBackgroundColorSpan(textShadowNode.backgroundColor)))
}
val roleIsLink =
if (textShadowNode.role != null)
textShadowNode.role == ReactAccessibilityDelegate.Role.LINK
else textShadowNode.accessibilityRole == AccessibilityRole.LINK
if (roleIsLink) {
ops.add(SetSpanOperation(start, end, ReactClickableSpan(textShadowNode.reactTag)))
}
val effectiveLetterSpacing = textAttributes.effectiveLetterSpacing
if (!java.lang.Float.isNaN(effectiveLetterSpacing) &&
(parentTextAttributes == null ||
parentTextAttributes.effectiveLetterSpacing != effectiveLetterSpacing)) {
ops.add(SetSpanOperation(start, end, CustomLetterSpacingSpan(effectiveLetterSpacing)))
}
val effectiveFontSize = textAttributes.effectiveFontSize
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything
// like `Float.NaN`.
parentTextAttributes == null ||
parentTextAttributes.effectiveFontSize != effectiveFontSize) {
ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize)))
}
if (textShadowNode.fontStyle != ReactConstants.UNSET ||
textShadowNode.fontWeight != ReactConstants.UNSET ||
textShadowNode.fontFamily != null) {
ops.add(
SetSpanOperation(
start,
end,
CustomStyleSpan(
textShadowNode.fontStyle,
textShadowNode.fontWeight,
textShadowNode.fontFeatureSettings,
textShadowNode.fontFamily,
textShadowNode.themedContext.assets)))
}
if (textShadowNode.isUnderlineTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactUnderlineSpan()))
}
if (textShadowNode.isLineThroughTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan()))
}
if ((textShadowNode.textShadowOffsetDx != 0f ||
textShadowNode.textShadowOffsetDy != 0f ||
textShadowNode.textShadowRadius != 0f) &&
Color.alpha(textShadowNode.textShadowColor) != 0) {
ops.add(
SetSpanOperation(
start,
end,
ShadowStyleSpan(
textShadowNode.textShadowOffsetDx,
textShadowNode.textShadowOffsetDy,
textShadowNode.textShadowRadius,
textShadowNode.textShadowColor)))
}
val effectiveLineHeight = textAttributes.effectiveLineHeight
if (!java.lang.Float.isNaN(effectiveLineHeight) &&
(parentTextAttributes == null ||
parentTextAttributes.effectiveLineHeight != effectiveLineHeight)) {
ops.add(SetSpanOperation(start, end, CustomLineHeightSpan(effectiveLineHeight)))
}
ops.add(SetSpanOperation(start, end, ReactTagSpan(textShadowNode.reactTag)))
}
}
}
}
@@ -66,15 +66,15 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
}
var layout = measureSpannedText(text, width, widthMode)
if (mAdjustsFontSizeToFit) {
val initialFontSize = mTextAttributes.effectiveFontSize
var currentFontSize = mTextAttributes.effectiveFontSize
if (adjustsFontSizeToFit) {
val initialFontSize = textAttributes.effectiveFontSize
var currentFontSize = textAttributes.effectiveFontSize
// Minimum font size is 4pts to match the iOS implementation.
val minimumFontSize =
max((mMinimumFontScale * initialFontSize), PixelUtil.toPixelFromDIP(4f)).toInt()
max((minimumFontScale * initialFontSize), PixelUtil.toPixelFromDIP(4f)).toInt()
while (currentFontSize > minimumFontSize &&
(mNumberOfLines != ReactConstants.UNSET && layout.lineCount > mNumberOfLines ||
heightMode != YogaMeasureMode.UNDEFINED && layout.height > height)) {
((numberOfLines != ReactConstants.UNSET && layout.lineCount > numberOfLines) ||
(heightMode != YogaMeasureMode.UNDEFINED && layout.height > height))) {
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
// measurements
// based on the number of points the font size needs to be reduced by.
@@ -111,8 +111,11 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
}
val lineCount =
if (mNumberOfLines == ReactConstants.UNSET) layout.lineCount
else min(mNumberOfLines.toDouble(), layout.lineCount.toDouble()).toInt()
if (numberOfLines == ReactConstants.UNSET) {
layout.lineCount
} else {
min(numberOfLines.toDouble(), layout.lineCount.toDouble()).toInt()
}
// Instead of using `layout.getWidth()` (which may yield a significantly larger width
// for
@@ -175,7 +178,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
var width = width
val textPaint = textPaintInstance
textPaint.textSize = mTextAttributes.effectiveFontSize.toFloat()
textPaint.textSize = textAttributes.effectiveFontSize.toFloat()
val layout: Layout
val boring = BoringLayout.isBoring(text, textPaint)
val desiredWidth = if (boring == null) Layout.getDesiredWidth(text, textPaint) else Float.NaN
@@ -184,7 +187,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
val unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0
val alignment =
when (textAlign) {
when (_textAlign) {
Gravity.LEFT -> Layout.Alignment.ALIGN_NORMAL
Gravity.RIGHT -> Layout.Alignment.ALIGN_OPPOSITE
Gravity.CENTER_HORIZONTAL -> Layout.Alignment.ALIGN_CENTER
@@ -201,12 +204,12 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, hintWidth)
.setAlignment(alignment)
.setLineSpacing(0f, 1f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(hyphenationFrequency)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(mJustificationMode)
builder.setJustificationMode(justificationMode)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(true)
@@ -224,7 +227,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
1f,
0f,
boring,
mIncludeFontPadding)
includeFontPadding)
} else {
// Is used for multiline, boring text and the width is known.
// Android 11+ introduces changes in text width calculation which leads to cases
@@ -238,12 +241,12 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, width.toInt())
.setAlignment(alignment)
.setLineSpacing(0f, 1f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(hyphenationFrequency)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(mJustificationMode)
builder.setJustificationMode(justificationMode)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -255,9 +258,9 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
}
// Return text alignment according to LTR or RTL style
private val textAlign: Int
private val _textAlign: Int
get() {
var textAlign = mTextAlign
var textAlign = super.textAlign
if (layoutDirection == YogaDirection.RTL) {
if (textAlign == Gravity.RIGHT) {
textAlign = Gravity.LEFT
@@ -300,14 +303,14 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
ReactTextUpdate(
text,
ReactConstants.UNSET,
mContainsImages,
containsImages,
getPadding(Spacing.START),
getPadding(Spacing.TOP),
getPadding(Spacing.END),
getPadding(Spacing.BOTTOM),
textAlign,
mTextBreakStrategy,
mJustificationMode)
_textAlign,
textBreakStrategy,
justificationMode)
uiViewOperationQueue.enqueueUpdateExtraData(reactTag, reactTextUpdate)
}
@@ -319,7 +322,9 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
override fun calculateLayoutOnChildren(): Iterable<ReactShadowNode<*>?>? {
// Run flexbox on and return the descendants which are inline views.
if (mInlineViews.isNullOrEmpty()) return null
if (inlineViews.isNullOrEmpty()) {
return null
}
val text: Spanned =
checkNotNull(preparedSpannableText) {
@@ -329,7 +334,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
val shadowNodes = mutableListOf<ReactShadowNode<*>?>()
for (placeholder in placeholders) {
val child = mInlineViews?.get(placeholder.reactTag)
val child = inlineViews?.get(placeholder.reactTag)
checkNotNull(child) { "Child is null" }
child.calculateLayout()
shadowNodes.add(child)
@@ -63,7 +63,7 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
}
init {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
setMeasureFunction(this)
}
@@ -110,15 +110,15 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
if (localData != null) {
localData?.apply(editText)
} else {
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.effectiveFontSize.toFloat())
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textAttributes.effectiveFontSize.toFloat())
if (mNumberOfLines != ReactConstants.UNSET) {
editText.setLines(mNumberOfLines)
if (numberOfLines != ReactConstants.UNSET) {
editText.setLines(numberOfLines)
}
@SuppressLint("WrongConstant")
if (editText.breakStrategy != mTextBreakStrategy) {
editText.breakStrategy = mTextBreakStrategy
if (editText.breakStrategy != textBreakStrategy) {
editText.breakStrategy = textBreakStrategy
}
}
@@ -152,12 +152,12 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
override fun setTextBreakStrategy(textBreakStrategy: String?) {
when (textBreakStrategy) {
null,
"simple" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
"highQuality" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
"balanced" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED
"simple" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
"highQuality" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
"balanced" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_BALANCED
else -> {
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: $textBreakStrategy")
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
this.textBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
}
}
}
@@ -175,14 +175,14 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
null // only needed to support inline views
),
mostRecentEventCount,
mContainsImages,
containsImages,
getPadding(Spacing.LEFT),
getPadding(Spacing.TOP),
getPadding(Spacing.RIGHT),
getPadding(Spacing.BOTTOM),
mTextAlign,
mTextBreakStrategy,
mJustificationMode)
textAlign,
textBreakStrategy,
justificationMode)
uiViewOperationQueue.enqueueUpdateExtraData(reactTag, reactTextUpdate)
}
}