diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c92611f9f2b..cb800c407c8 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 ()V public fun (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 (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 { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java deleted file mode 100644 index 5bdd5cbaba7..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ /dev/null @@ -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. - * - *

This class handles all text attributes associated with {@code }-ish node. A concrete - * node can be an anchor {@code } node, an anchor {@code } node or virtual {@code - * } node inside {@code } or {@code } node. Or even something else. - * - *

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 ops, - @Nullable TextAttributes parentTextAttributes, - boolean supportsInlineViews, - @Nullable Map inlineViews, - int start) { - - TextAttributes textAttributes; - if (parentTextAttributes != null) { - textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes); - } else { - textAttributes = textShadowNode.mTextAttributes; - } - - 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 or 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 ops = new ArrayList<>(); - Map 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. - * - *

Example, Android 4.4: - * - *

-   * Bold Text
-   *   Bold Text
-   *     Bold Text
-   *
-   * Not Bold Text
-   *   Not Bold Text
-   *     Not Bold Text
-   *
-   * Not Bold Text
-   *   Bold Text
-   *     Bold Text
-   * 
- */ - 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 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(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.kt new file mode 100644 index 00000000000..a392240bac6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.kt @@ -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 ``-ish node. A concrete node can be + * an anchor `` node, an anchor `` node or virtual `` node inside `` or + * `` 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: + *
+   * Bold Text
+   * Bold Text
+   * Bold Text
+   *
+   * Not Bold Text
+   * Not Bold Text
+   * Not Bold Text
+   *
+   * Not Bold Text
+   * Bold Text
+   * Bold Text
+   * 
+ */ + 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>? = 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 = ArrayList() + val inlineViews: MutableMap>? = + 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).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, + parentTextAttributes: TextAttributes?, + supportsInlineViews: Boolean, + inlineViews: MutableMap>?, + 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 or 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))) + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.kt index e0fc35e08bc..d09cef396bd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.kt @@ -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?>? { // 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?>() 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) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.kt index 28c44f9b1b6..af00dc79bb3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.kt @@ -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) } }