From 7bcdb23cf06997e50fee7cca41735b77881ee144 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Fri, 19 Jan 2024 17:24:52 -0800 Subject: [PATCH] De-duplicate building `Spannable` on Android (#39630) Summary: A first step in my work on https://github.com/react-native-community/discussions-and-proposals/issues/695 De-duplicate the code for creating `Spannable` on Android. I'm planning to add quite serious new features to this module. This would be really hard with the current level of code duplication. ## Changelog: [INTERNAL] [CHANGED] - De-duplicate building `Spannable` on Android Pull Request resolved: https://github.com/facebook/react-native/pull/39630 Test Plan: I tried to ensure that the refactored code is relatively easy to prove to be equivalent to the original duplicated one, but there's always a risk of a human mistake in this process. So far, I have been testing this by ensuring that nothing broke in the `Text` example section in RNTester. Reviewed By: mdvacca Differential Revision: D51016244 Pulled By: NickGerleman fbshipit-source-id: e9f873c01b2af0685c7b0943aebea170c997d22e --- .../ReactAndroid/api/ReactAndroid.api | 51 +++- .../facebook/react/common/ReactConstants.java | 8 + .../react/common/assets/ReactFontManager.java | 12 +- .../react/config/ReactFeatureFlags.java | 3 + .../views/text/BasicTextAttributeProvider.kt | 48 +++ .../react/views/text/CustomStyleSpan.java | 7 +- .../text/EffectiveTextAttributeProvider.kt | 22 ++ .../text/HierarchicTextAttributeProvider.kt | 63 ++++ .../views/text/ReactBaseTextShadowNode.java | 212 ++++++++++++- .../react/views/text/ReactTextShadowNode.java | 7 +- .../react/views/text/ReactTextUpdate.java | 11 +- .../react/views/text/ReactTextView.java | 10 +- .../react/views/text/ReactTypefaceUtils.java | 5 +- .../react/views/text/SetSpanOperation.java | 2 +- .../react/views/text/TextAttributeProps.java | 137 +++++++-- .../react/views/text/TextAttributes.java | 6 +- .../react/views/text/TextLayoutManager.java | 40 ++- .../text/TextLayoutManagerMapBuffer.java | 33 ++- .../react/views/text/TextLayoutUtils.kt | 280 ++++++++++++++++++ .../react/views/text/TextTransform.java | 3 +- .../text/fragments/BridgeTextFragment.kt | 39 +++ .../text/fragments/BridgeTextFragmentList.kt | 18 ++ .../text/fragments/MapBufferTextFragment.kt | 42 +++ .../fragments/MapBufferTextFragmentList.kt | 19 ++ .../views/text/fragments/TextFragment.kt | 29 ++ .../views/text/fragments/TextFragmentList.kt | 15 + .../react/views/textinput/ReactEditText.java | 17 +- .../textinput/ReactTextInputShadowNode.java | 6 +- 28 files changed, 1051 insertions(+), 94 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f709de483e5..79953f0c65e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1686,6 +1686,7 @@ public final class com/facebook/react/common/MapBuilder$Builder { public class com/facebook/react/common/ReactConstants { public static final field TAG Ljava/lang/String; + public static final field UNSET I public fun ()V } @@ -1764,7 +1765,6 @@ public class com/facebook/react/common/assets/ReactFontManager { public class com/facebook/react/common/assets/ReactFontManager$TypefaceStyle { public static final field BOLD I public static final field NORMAL I - public static final field UNSET I public fun (I)V public fun (II)V public fun (IZ)V @@ -1906,6 +1906,7 @@ public class com/facebook/react/config/ReactFeatureFlags { public static field enableMountHooks Z public static field enableOnDemandReactChoreographer Z public static field enableRemoveDeleteTreeInstruction Z + public static field enableSpannableBuildingUnification Z public static field enableTextSpannableCache Z public static field enableViewRecycling Z public static field excludeYogaFromRawProps Z @@ -6785,7 +6786,7 @@ public class com/facebook/react/views/text/ReactBackgroundColorSpan : android/te public fun (I)V } -public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode { +public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode, com/facebook/react/views/text/BasicTextAttributeProvider { public static final field DEFAULT_TEXT_SHADOW_COLOR I public static final field PROP_SHADOW_COLOR Ljava/lang/String; public static final field PROP_SHADOW_OFFSET Ljava/lang/String; @@ -6793,7 +6794,6 @@ public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : co public static final field PROP_SHADOW_OFFSET_WIDTH Ljava/lang/String; public static final field PROP_SHADOW_RADIUS Ljava/lang/String; public static final field PROP_TEXT_TRANSFORM Ljava/lang/String; - public static final field UNSET I protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; protected field mAdjustsFontSizeToFit Z protected field mBackgroundColor I @@ -6824,6 +6824,22 @@ public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : co protected field mTextShadowRadius F public fun ()V public fun (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V + public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; + public fun getBackgroundColor ()I + public fun getColor ()I + public fun getFontFamily ()Ljava/lang/String; + public fun getFontFeatureSettings ()Ljava/lang/String; + public fun getFontStyle ()I + public fun getFontWeight ()I + public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; + public fun getTextShadowColor ()I + public fun getTextShadowOffsetDx ()F + public fun getTextShadowOffsetDy ()F + public fun getTextShadowRadius ()F + public fun isBackgroundColorSet ()Z + public fun isColorSet ()Z + public fun isLineThroughTextDecorationSet ()Z + public fun isUnderlineTextDecorationSet ()Z public fun setAccessibilityRole (Ljava/lang/String;)V public fun setAdjustFontSizeToFit (Z)V public fun setAllowFontScaling (Z)V @@ -7084,12 +7100,19 @@ public class com/facebook/react/views/text/ReactVirtualTextViewManager$$PropsSet public fun setProperty (Lcom/facebook/react/views/text/ReactVirtualTextViewManager;Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V } +public class com/facebook/react/views/text/SetSpanOperation { + protected field end I + protected field start I + protected field what Lcom/facebook/react/views/text/ReactSpan; + public fun execute (Landroid/text/SpannableStringBuilder;I)V +} + public class com/facebook/react/views/text/ShadowStyleSpan : android/text/style/CharacterStyle, com/facebook/react/views/text/ReactSpan { public fun (FFFI)V public fun updateDrawState (Landroid/text/TextPaint;)V } -public class com/facebook/react/views/text/TextAttributeProps { +public class com/facebook/react/views/text/TextAttributeProps : com/facebook/react/views/text/EffectiveTextAttributeProvider { public static final field TA_KEY_ACCESSIBILITY_ROLE S public static final field TA_KEY_ALIGNMENT S public static final field TA_KEY_ALLOW_FONT_SCALING S @@ -7117,7 +7140,6 @@ public class com/facebook/react/views/text/TextAttributeProps { public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY S public static final field TA_KEY_TEXT_SHADOW_RADIUS S public static final field TA_KEY_TEXT_TRANSFORM S - public static final field UNSET I protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; protected field mAllowFontScaling Z protected field mBackgroundColor I @@ -7149,13 +7171,32 @@ public class com/facebook/react/views/text/TextAttributeProps { protected field mTextTransform Lcom/facebook/react/views/text/TextTransform; public static fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps; public static fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps; + public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; + public fun getBackgroundColor ()I + public fun getColor ()I + public fun getEffectiveFontSize ()I + public fun getEffectiveLetterSpacing ()F public fun getEffectiveLineHeight ()F + public fun getFontFamily ()Ljava/lang/String; + public fun getFontFeatureSettings ()Ljava/lang/String; + public fun getFontStyle ()I + public fun getFontWeight ()I public static fun getHyphenationFrequency (Ljava/lang/String;)I public static fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I public static fun getLayoutDirection (Ljava/lang/String;)I public fun getLetterSpacing ()F + public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; public static fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I public static fun getTextBreakStrategy (Ljava/lang/String;)I + public fun getTextShadowColor ()I + public fun getTextShadowOffsetDx ()F + public fun getTextShadowOffsetDy ()F + public fun getTextShadowRadius ()F + public fun getTextTransform ()Lcom/facebook/react/views/text/TextTransform; + public fun isBackgroundColorSet ()Z + public fun isColorSet ()Z + public fun isLineThroughTextDecorationSet ()Z + public fun isUnderlineTextDecorationSet ()Z } public class com/facebook/react/views/text/TextAttributes { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java index 3f438506daa..413ce8a7ce9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java @@ -10,4 +10,12 @@ package com.facebook.react.common; public class ReactConstants { public static final String TAG = "ReactNative"; + + /** + * Some types have built-in support for representing a "missing" or "unset" value, for example NaN + * in the case of floating point numbers or null in the case of object references. Integers don't + * have such a special value. When an integer represent an inherently non-negative value, we use a + * special negative value to mark it as "unset". + */ + public static final int UNSET = -1; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java index 7e7df01bd14..62efef79e73 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java @@ -15,6 +15,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.common.ReactConstants; import java.util.HashMap; import java.util.Map; @@ -167,8 +168,6 @@ public class ReactFontManager { public static final int BOLD = 700; public static final int NORMAL = 400; - public static final int UNSET = -1; - private static final int MIN_WEIGHT = 1; private static final int MAX_WEIGHT = 1000; @@ -177,11 +176,11 @@ public class ReactFontManager { public TypefaceStyle(int weight, boolean italic) { mItalic = italic; - mWeight = weight == UNSET ? NORMAL : weight; + mWeight = weight == ReactConstants.UNSET ? NORMAL : weight; } public TypefaceStyle(int style) { - if (style == UNSET) { + if (style == ReactConstants.UNSET) { style = Typeface.NORMAL; } @@ -194,12 +193,13 @@ public class ReactFontManager { * existing weight bit in `style` will be used. */ public TypefaceStyle(int style, int weight) { - if (style == UNSET) { + if (style == ReactConstants.UNSET) { style = Typeface.NORMAL; } mItalic = (style & Typeface.ITALIC) != 0; - mWeight = weight == UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight; + mWeight = + weight == ReactConstants.UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight; } public int getNearestStyle() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 250772c9015..ee4882d37f5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -173,4 +173,7 @@ public class ReactFeatureFlags { * when there is work to do. */ public static boolean enableOnDemandReactChoreographer = false; + + /** Enables the new unified {@link android.text.Spannable} building logic. */ + public static boolean enableSpannableBuildingUnification = false; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt new file mode 100644 index 00000000000..bf9693f5971 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import com.facebook.react.uimanager.ReactAccessibilityDelegate + +/** + * Interface for an entity providing basic text attributes of a text node/fragment. "Basic" means + * that they can be provided trivially, without processing the parent element. + */ +internal interface BasicTextAttributeProvider { + val role: ReactAccessibilityDelegate.Role? + + val accessibilityRole: ReactAccessibilityDelegate.AccessibilityRole? + + val isBackgroundColorSet: Boolean + + val backgroundColor: Int + + val isColorSet: Boolean + + val color: Int + + val fontStyle: Int + + val fontWeight: Int + + val fontFamily: String? + + val fontFeatureSettings: String? + + val isUnderlineTextDecorationSet: Boolean + + val isLineThroughTextDecorationSet: Boolean + + val textShadowOffsetDx: Float + + val textShadowOffsetDy: Float + + val textShadowRadius: Float + + val textShadowColor: Int +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java index 51fd4dd0149..1bcbb6f919f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java @@ -14,6 +14,7 @@ import android.text.TextPaint; import android.text.style.MetricAffectingSpan; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.assets.ReactFontManager; @Nullsafe(Nullsafe.Mode.LOCAL) @@ -61,13 +62,11 @@ public class CustomStyleSpan extends MetricAffectingSpan implements ReactSpan { } public int getStyle() { - return mStyle == ReactFontManager.TypefaceStyle.UNSET ? Typeface.NORMAL : mStyle; + return mStyle == ReactConstants.UNSET ? Typeface.NORMAL : mStyle; } public int getWeight() { - return mWeight == ReactFontManager.TypefaceStyle.UNSET - ? ReactFontManager.TypefaceStyle.NORMAL - : mWeight; + return mWeight == ReactConstants.UNSET ? ReactFontManager.TypefaceStyle.NORMAL : mWeight; } public @Nullable String getFontFamily() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt new file mode 100644 index 00000000000..78b3562785d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import com.facebook.react.common.ReactConstants.UNSET + +/** Interface for an entity providing effective text attributes of a text node/fragment */ +internal interface EffectiveTextAttributeProvider : BasicTextAttributeProvider { + val textTransform: TextTransform + + val effectiveLetterSpacing: Float + + /** @return The effective font size, or [UNSET] if not set */ + val effectiveFontSize: Int + + val effectiveLineHeight: Float +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt new file mode 100644 index 00000000000..b322c08bb38 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import com.facebook.react.common.ReactConstants + +/** + * Implementation of [EffectiveTextAttributeProvider] that provides effective text attributes based + * on a [ReactBaseTextShadowNode] instance and its parent. + */ +internal class HierarchicTextAttributeProvider( + private val textShadowNode: ReactBaseTextShadowNode, + private val parentTextAttributes: TextAttributes?, + private val textAttributes: TextAttributes +) : EffectiveTextAttributeProvider, BasicTextAttributeProvider by textShadowNode { + override val textTransform: TextTransform + get() = textAttributes.textTransform + + override val effectiveLetterSpacing: Float + get() { + val letterSpacing = textAttributes.effectiveLetterSpacing + + val isParentLetterSpacingDifferent = + parentTextAttributes == null || + parentTextAttributes.effectiveLetterSpacing != letterSpacing + + return if (!letterSpacing.isNaN() && isParentLetterSpacingDifferent) { + letterSpacing + } else { + Float.NaN + } + } + + override val effectiveFontSize: Int + get() { + val fontSize = textAttributes.effectiveFontSize + + return if (parentTextAttributes == null || + parentTextAttributes.effectiveFontSize != fontSize) { + fontSize + } else { + ReactConstants.UNSET + } + } + + override val effectiveLineHeight: Float + get() { + val lineHeight = textAttributes.effectiveLineHeight + val isParentLineHeightDifferent = + parentTextAttributes == null || parentTextAttributes.effectiveLineHeight != lineHeight + + return if (!lineHeight.isNaN() && isParentLineHeightDifferent) { + lineHeight + } else { + Float.NaN + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index c752e04be06..2c0489df363 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -21,7 +21,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.assets.ReactFontManager; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; @@ -49,13 +49,13 @@ import java.util.Map; *

This also node calculates {@link Spannable} object based on subnodes of the same type, which * can be used in concrete classes to feed native views and compute layout. */ -public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { +public abstract class ReactBaseTextShadowNode extends LayoutShadowNode + implements BasicTextAttributeProvider { // Use a direction weak character so the placeholder doesn't change the direction of the previous // character. // https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters private static final String INLINE_VIEW_PLACEHOLDER = "0"; - public static final int UNSET = ReactFontManager.TypefaceStyle.UNSET; public static final String PROP_SHADOW_OFFSET = "textShadowOffset"; public static final String PROP_SHADOW_OFFSET_WIDTH = "width"; @@ -77,6 +77,23 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { boolean supportsInlineViews, @Nullable Map inlineViews, int start) { + if (ReactFeatureFlags.enableSpannableBuildingUnification) { + buildSpannedFromShadowNodeUnified( + textShadowNode, sb, ops, parentTextAttributes, supportsInlineViews, inlineViews, start); + } else { + buildSpannedFromShadowNodeDuplicated( + textShadowNode, sb, ops, parentTextAttributes, supportsInlineViews, inlineViews, start); + } + } + + private static void buildSpannedFromShadowNodeDuplicated( + ReactBaseTextShadowNode textShadowNode, + SpannableStringBuilder sb, + List ops, + @Nullable TextAttributes parentTextAttributes, + boolean supportsInlineViews, + @Nullable Map inlineViews, + int start) { TextAttributes textAttributes; if (parentTextAttributes != null) { @@ -93,7 +110,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { TextTransform.apply( ((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform())); } else if (child instanceof ReactBaseTextShadowNode) { - buildSpannedFromShadowNode( + buildSpannedFromShadowNodeDuplicated( (ReactBaseTextShadowNode) child, sb, ops, @@ -177,8 +194,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { || parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) { ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); } - if (textShadowNode.mFontStyle != UNSET - || textShadowNode.mFontWeight != UNSET + if (textShadowNode.mFontStyle != ReactConstants.UNSET + || textShadowNode.mFontWeight != ReactConstants.UNSET || textShadowNode.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -221,6 +238,99 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + private static void buildSpannedFromShadowNodeUnified( + ReactBaseTextShadowNode textShadowNode, + SpannableStringBuilder sb, + List ops, + @Nullable TextAttributes parentTextAttributes, + boolean supportsInlineViews, + @Nullable Map inlineViews, + int start) { + + TextAttributes textAttributes; + if (parentTextAttributes != null) { + textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes); + } else { + textAttributes = textShadowNode.mTextAttributes; + } + + final HierarchicTextAttributeProvider textAttributeProvider = + new HierarchicTextAttributeProvider(textShadowNode, parentTextAttributes, textAttributes); + + for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) { + ReactShadowNode child = textShadowNode.getChildAt(i); + + if (child instanceof ReactRawTextShadowNode) { + TextLayoutUtils.addText( + sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider); + } else if (child instanceof ReactBaseTextShadowNode) { + buildSpannedFromShadowNodeUnified( + (ReactBaseTextShadowNode) child, + sb, + ops, + textAttributes, + supportsInlineViews, + inlineViews, + sb.length()); + } else if (child instanceof ReactTextInlineImageShadowNode) { + addInlineImageSpan(ops, sb, (ReactTextInlineImageShadowNode) child); + } else if (supportsInlineViews) { + addInlineViewPlaceholderSpan(ops, sb, child); + + inlineViews.put(child.getReactTag(), child); + } else { + throw new IllegalViewOperationException( + "Unexpected view type nested under a or node: " + child.getClass()); + } + child.markUpdateSeen(); + } + int end = sb.length(); + if (end >= start) { + final int reactTag = textShadowNode.getReactTag(); + + TextLayoutUtils.addApplicableTextAttributeSpans( + ops, textAttributeProvider, reactTag, textShadowNode.getThemedContext(), start, end); + } + } + + private static void addInlineImageSpan( + List ops, SpannableStringBuilder sb, ReactTextInlineImageShadowNode child) { + // We make the image take up 1 character in the span and put a corresponding character into + // the text so that the image doesn't run over any following text. + sb.append(INLINE_VIEW_PLACEHOLDER); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + child.buildInlineImageSpan())); + } + + private static void addInlineViewPlaceholderSpan( + List ops, SpannableStringBuilder sb, ReactShadowNode child) { + YogaValue widthValue = child.getStyleWidth(); + YogaValue heightValue = child.getStyleHeight(); + + float width; + float height; + if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) { + // If the measurement of the child isn't calculated, we calculate the layout for the + // view using Yoga + child.calculateLayout(); + width = child.getLayoutWidth(); + height = child.getLayoutHeight(); + } else { + width = widthValue.value; + height = heightValue.value; + } + + // We make the inline view take up 1 character in the span and put a corresponding character + // into the text so that + // the inline view doesn't run over any following text. + sb.append(INLINE_VIEW_PLACEHOLDER); + + TextLayoutUtils.addInlineViewPlaceholderSpan(ops, sb, child.getReactTag(), width, height); + } + // `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`. protected Spannable spannedFromShadowNode( ReactBaseTextShadowNode textShadowNode, @@ -307,7 +417,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; - protected int mNumberOfLines = UNSET; + protected int mNumberOfLines = ReactConstants.UNSET; protected int mTextAlign = Gravity.NO_GRAVITY; protected int mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; protected int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; @@ -329,9 +439,9 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link * Typeface#NORMAL} or {@link Typeface#BOLD}. */ - protected int mFontStyle = UNSET; + protected int mFontStyle = ReactConstants.UNSET; - protected int mFontWeight = UNSET; + protected int mFontWeight = ReactConstants.UNSET; /** * NB: If a font family is used that does not have a style in a certain Android version (ie. * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text @@ -384,9 +494,9 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { return textAlign; } - @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = UNSET) + @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET) public void setNumberOfLines(int numberOfLines) { - mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; + mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines; markUpdated(); } @@ -452,6 +562,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { markUpdated(); } + @Override + public int getColor() { + return mColor; + } + @ReactProp(name = ViewProps.COLOR, customType = "Color") public void setColor(@Nullable Integer color) { mIsColorSet = (color != null); @@ -461,6 +576,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { markUpdated(); } + @Override + public boolean isColorSet() { + return mIsColorSet; + } + + @Override + public int getBackgroundColor() { + return mBackgroundColor; + } + @ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color") public void setBackgroundColor(@Nullable Integer color) { // Background color needs to be handled here for virtual nodes so it can be incorporated into @@ -476,6 +601,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + @Override + public boolean isBackgroundColorSet() { + return mIsBackgroundColorSet; + } + + @Override + public @Nullable AccessibilityRole getAccessibilityRole() { + return mAccessibilityRole; + } + @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) public void setAccessibilityRole(@Nullable String accessibilityRole) { if (isVirtual()) { @@ -484,6 +619,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + @Override + public @Nullable Role getRole() { + return mRole; + } + @ReactProp(name = ViewProps.ROLE) public void setRole(@Nullable String role) { if (isVirtual()) { @@ -492,12 +632,22 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + @Override + public String getFontFamily() { + return mFontFamily; + } + @ReactProp(name = ViewProps.FONT_FAMILY) public void setFontFamily(@Nullable String fontFamily) { mFontFamily = fontFamily; markUpdated(); } + @Override + public int getFontWeight() { + return mFontWeight; + } + @ReactProp(name = ViewProps.FONT_WEIGHT) public void setFontWeight(@Nullable String fontWeightString) { int fontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString); @@ -517,6 +667,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + @Override + public String getFontFeatureSettings() { + return mFontFeatureSettings; + } + + @Override + public int getFontStyle() { + return mFontStyle; + } + @ReactProp(name = ViewProps.FONT_STYLE) public void setFontStyle(@Nullable String fontStyleString) { int fontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString); @@ -547,6 +707,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { markUpdated(); } + @Override + public boolean isUnderlineTextDecorationSet() { + return mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return mIsLineThroughTextDecorationSet; + } + @ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY) public void setTextBreakStrategy(@Nullable String textBreakStrategy) { if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) { @@ -584,6 +754,21 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { markUpdated(); } + @Override + public float getTextShadowOffsetDx() { + return mTextShadowOffsetDx; + } + + @Override + public float getTextShadowOffsetDy() { + return mTextShadowOffsetDy; + } + + @Override + public float getTextShadowRadius() { + return mTextShadowRadius; + } + @ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1) public void setTextShadowRadius(float textShadowRadius) { if (textShadowRadius != mTextShadowRadius) { @@ -592,6 +777,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { } } + @Override + public int getTextShadowColor() { + return mTextShadowColor; + } + @ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color") public void setTextShadowColor(int textShadowColor) { if (textShadowColor != mTextShadowColor) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index d53f18a5aa3..9b7d15715c9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactShadowNode; @@ -80,7 +81,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { int minimumFontSize = (int) Math.max(mMinimumFontScale * initialFontSize, PixelUtil.toPixelFromDIP(4)); while (currentFontSize > minimumFontSize - && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines + && (mNumberOfLines != ReactConstants.UNSET && layout.getLineCount() > mNumberOfLines || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) { // TODO: We could probably use a smarter algorithm here. This will require 0(n) // measurements @@ -122,7 +123,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { } final int lineCount = - mNumberOfLines == UNSET + mNumberOfLines == ReactConstants.UNSET ? layout.getLineCount() : Math.min(mNumberOfLines, layout.getLineCount()); @@ -327,7 +328,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( mPreparedSpannableText, - UNSET, + ReactConstants.UNSET, mContainsImages, getPadding(Spacing.START), getPadding(Spacing.TOP), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 0802ec3622a..d2230beda0e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -7,10 +7,9 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.text.Layout; import android.text.Spannable; +import com.facebook.react.common.ReactConstants; /** * Class that contains the data needed for a text update. Used by both and @@ -67,10 +66,10 @@ public class ReactTextUpdate { text, jsEventCounter, containsImages, - UNSET, - UNSET, - UNSET, - UNSET, + ReactConstants.UNSET, + ReactConstants.UNSET, + ReactConstants.UNSET, + ReactConstants.UNSET, textAlign, textBreakStrategy, justificationMode); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 0c1aa6a19cd..d0343a59e55 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -7,8 +7,6 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; @@ -376,10 +374,10 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie // In Fabric padding is set by the update of Layout Metrics and not as part of the "setText" // operation // TODO T56559197: remove this condition when we migrate 100% to Fabric - if (paddingLeft != UNSET - && paddingTop != UNSET - && paddingRight != UNSET - && paddingBottom != UNSET) { + if (paddingLeft != ReactConstants.UNSET + && paddingTop != ReactConstants.UNSET + && paddingRight != ReactConstants.UNSET + && paddingBottom != ReactConstants.UNSET) { setPadding( (int) Math.floor(paddingLeft), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java index 64fe5e36c0e..061db50fcf0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java @@ -13,6 +13,7 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Nullsafe; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.assets.ReactFontManager; import java.util.ArrayList; import java.util.List; @@ -45,7 +46,7 @@ public class ReactTypefaceUtils { return 900; } } - return ReactFontManager.TypefaceStyle.UNSET; + return ReactConstants.UNSET; } public static int parseFontStyle(@Nullable String fontStyleString) { @@ -57,7 +58,7 @@ public class ReactTypefaceUtils { return Typeface.NORMAL; } } - return ReactFontManager.TypefaceStyle.UNSET; + return ReactConstants.UNSET; } public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java index 5df16e4e283..57649508d54 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java @@ -12,7 +12,7 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import com.facebook.common.logging.FLog; -class SetSpanOperation { +public class SetSpanOperation { private static final String TAG = "SetSpanOperation"; static final int SPAN_MAX_PRIORITY = Spanned.SPAN_PRIORITY >> Spanned.SPAN_PRIORITY_SHIFT; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index aa450d79fd2..25791e00679 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -12,6 +12,7 @@ import android.text.Layout; import android.text.TextUtils; import android.util.LayoutDirection; import android.view.Gravity; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.ReadableArray; @@ -30,7 +31,7 @@ import java.util.List; // TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents // TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to // TextAttributeProps (e.g. TextAlign) -public class TextAttributeProps { +public class TextAttributeProps implements EffectiveTextAttributeProvider { // constants for Text Attributes serialization public static final short TA_KEY_FOREGROUND_COLOR = 0; @@ -61,8 +62,6 @@ public class TextAttributeProps { public static final short TA_KEY_ROLE = 26; public static final short TA_KEY_TEXT_TRANSFORM = 27; - public static final int UNSET = -1; - private static final String PROP_SHADOW_OFFSET = "textShadowOffset"; private static final String PROP_SHADOW_OFFSET_WIDTH = "width"; private static final String PROP_SHADOW_OFFSET_HEIGHT = "height"; @@ -84,17 +83,17 @@ public class TextAttributeProps { protected boolean mIsBackgroundColorSet = false; protected int mBackgroundColor; - protected int mNumberOfLines = UNSET; - protected int mFontSize = UNSET; - protected float mFontSizeInput = UNSET; - protected float mLineHeightInput = UNSET; + protected int mNumberOfLines = ReactConstants.UNSET; + protected int mFontSize = ReactConstants.UNSET; + protected float mFontSizeInput = ReactConstants.UNSET; + protected float mLineHeightInput = ReactConstants.UNSET; protected float mLetterSpacingInput = Float.NaN; protected int mTextAlign = Gravity.NO_GRAVITY; - // `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available. - protected int mLayoutDirection = UNSET; + // `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol) + protected int mLayoutDirection = ReactConstants.UNSET; - protected TextTransform mTextTransform = TextTransform.NONE; + @NonNull protected TextTransform mTextTransform = TextTransform.NONE; protected float mTextShadowOffsetDx = 0; protected float mTextShadowOffsetDy = 0; @@ -108,8 +107,8 @@ public class TextAttributeProps { protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; - protected int mFontStyle = UNSET; - protected int mFontWeight = UNSET; + protected int mFontStyle = ReactConstants.UNSET; + protected int mFontWeight = ReactConstants.UNSET; /** * NB: If a font family is used that does not have a style in a certain Android version (ie. * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text @@ -233,11 +232,11 @@ public class TextAttributeProps { public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { TextAttributeProps result = new TextAttributeProps(); - result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, UNSET)); - result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, UNSET)); + result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, ReactConstants.UNSET)); + result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET)); result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); - result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, UNSET)); + result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET)); result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); result.setColor( props.hasKey(ViewProps.FOREGROUND_COLOR) @@ -343,6 +342,7 @@ public class TextAttributeProps { // Returns a line height which takes into account the requested line height // and the height of the inline images. + @Override public float getEffectiveLineHeight() { boolean useInlineViewHeight = !Float.isNaN(mLineHeight) @@ -352,12 +352,12 @@ public class TextAttributeProps { } private void setNumberOfLines(int numberOfLines) { - mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; + mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines; } private void setLineHeight(float lineHeight) { mLineHeightInput = lineHeight; - if (lineHeight == UNSET) { + if (lineHeight == ReactConstants.UNSET) { mLineHeight = Float.NaN; } else { mLineHeight = @@ -371,6 +371,12 @@ public class TextAttributeProps { mLetterSpacingInput = letterSpacing; } + @Override + @NonNull + public TextTransform getTextTransform() { + return mTextTransform; + } + public float getLetterSpacing() { float letterSpacingPixels = mAllowFontScaling @@ -386,6 +392,16 @@ public class TextAttributeProps { return letterSpacingPixels / mFontSize; } + @Override + public float getEffectiveLetterSpacing() { + return getLetterSpacing(); + } + + @Override + public int getEffectiveFontSize() { + return mFontSize; + } + private void setAllowFontScaling(boolean allowFontScaling) { if (allowFontScaling != mAllowFontScaling) { mAllowFontScaling = allowFontScaling; @@ -397,7 +413,7 @@ public class TextAttributeProps { private void setFontSize(float fontSize) { mFontSizeInput = fontSize; - if (fontSize != UNSET) { + if (fontSize != ReactConstants.UNSET) { fontSize = mAllowFontScaling ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize)) @@ -406,6 +422,11 @@ public class TextAttributeProps { mFontSize = (int) fontSize; } + @Override + public int getColor() { + return mColor; + } + private void setColor(@Nullable Integer color) { mIsColorSet = (color != null); if (mIsColorSet) { @@ -413,6 +434,16 @@ public class TextAttributeProps { } } + @Override + public boolean isColorSet() { + return mIsColorSet; + } + + @Override + public int getBackgroundColor() { + return mBackgroundColor; + } + private void setBackgroundColor(Integer color) { // TODO: Don't apply background color to anchor TextView since it will be applied on the View // directly @@ -424,6 +455,21 @@ public class TextAttributeProps { // } } + @Override + public boolean isBackgroundColorSet() { + return mIsBackgroundColorSet; + } + + @Override + public int getFontStyle() { + return mFontStyle; + } + + @Override + public String getFontFamily() { + return mFontFamily; + } + private void setFontFamily(@Nullable String fontFamily) { mFontFamily = fontFamily; } @@ -526,6 +572,16 @@ public class TextAttributeProps { mFontFeatureSettings = TextUtils.join(", ", features); } + @Override + public String getFontFeatureSettings() { + return mFontFeatureSettings; + } + + @Override + public int getFontWeight() { + return mFontWeight; + } + private void setFontWeight(@Nullable String fontWeightString) { mFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString); } @@ -552,6 +608,16 @@ public class TextAttributeProps { } } + @Override + public boolean isUnderlineTextDecorationSet() { + return mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return mIsLineThroughTextDecorationSet; + } + private void setTextShadowOffset(ReadableMap offsetMap) { mTextShadowOffsetDx = 0; mTextShadowOffsetDy = 0; @@ -570,10 +636,20 @@ public class TextAttributeProps { } } + @Override + public float getTextShadowOffsetDx() { + return mTextShadowOffsetDx; + } + private void setTextShadowOffsetDx(float dx) { mTextShadowOffsetDx = PixelUtil.toPixelFromDIP(dx); } + @Override + public float getTextShadowOffsetDy() { + return mTextShadowOffsetDy; + } + private void setTextShadowOffsetDy(float dy) { mTextShadowOffsetDy = PixelUtil.toPixelFromDIP(dy); } @@ -581,14 +657,14 @@ public class TextAttributeProps { public static int getLayoutDirection(@Nullable String layoutDirection) { int androidLayoutDirection; if (layoutDirection == null || "undefined".equals(layoutDirection)) { - androidLayoutDirection = UNSET; + androidLayoutDirection = ReactConstants.UNSET; } else if ("rtl".equals(layoutDirection)) { androidLayoutDirection = LayoutDirection.RTL; } else if ("ltr".equals(layoutDirection)) { androidLayoutDirection = LayoutDirection.LTR; } else { FLog.w(ReactConstants.TAG, "Invalid layoutDirection: " + layoutDirection); - androidLayoutDirection = UNSET; + androidLayoutDirection = ReactConstants.UNSET; } return androidLayoutDirection; } @@ -597,12 +673,22 @@ public class TextAttributeProps { mLayoutDirection = getLayoutDirection(layoutDirection); } + @Override + public float getTextShadowRadius() { + return mTextShadowRadius; + } + private void setTextShadowRadius(float textShadowRadius) { if (textShadowRadius != mTextShadowRadius) { mTextShadowRadius = textShadowRadius; } } + @Override + public int getTextShadowColor() { + return mTextShadowColor; + } + private void setTextShadowColor(int textShadowColor) { if (textShadowColor != mTextShadowColor) { mTextShadowColor = textShadowColor; @@ -624,6 +710,11 @@ public class TextAttributeProps { } } + @Override + public AccessibilityRole getAccessibilityRole() { + return mAccessibilityRole; + } + private void setAccessibilityRole(@Nullable String accessibilityRole) { if (accessibilityRole == null) { mAccessibilityRole = null; @@ -632,6 +723,12 @@ public class TextAttributeProps { } } + @Nullable + @Override + public Role getRole() { + return mRole; + } + private void setRole(@Nullable String role) { if (role == null) { mRole = null; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java index 8d54a23bbdf..de72c0e3914 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import androidx.annotation.NonNull; import com.facebook.common.logging.FLog; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.PixelUtil; @@ -29,7 +30,8 @@ public class TextAttributes { private float mLetterSpacing = Float.NaN; private float mMaxFontSizeMultiplier = Float.NaN; private float mHeightOfTallestInlineViewOrImage = Float.NaN; - private TextTransform mTextTransform = TextTransform.UNSET; + + @NonNull private TextTransform mTextTransform = TextTransform.UNSET; public TextAttributes() {} @@ -118,7 +120,7 @@ public class TextAttributes { return mTextTransform; } - public void setTextTransform(TextTransform textTransform) { + public void setTextTransform(@NonNull TextTransform textTransform) { mTextTransform = textTransform; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 93eda5d06b3..439401492d9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -7,8 +7,6 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.content.Context; import android.graphics.Color; import android.os.Build; @@ -31,12 +29,15 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.views.text.fragments.BridgeTextFragmentList; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaMeasureMode; import com.facebook.yoga.YogaMeasureOutput; @@ -99,7 +100,19 @@ public class TextLayoutManager { sTagToSpannableCache.remove(reactTag); } - private static void buildSpannableFromFragment( + private static void buildSpannableFromFragments( + Context context, + ReadableArray fragments, + SpannableStringBuilder sb, + List ops) { + if (ReactFeatureFlags.enableSpannableBuildingUnification) { + buildSpannableFromFragmentsUnified(context, fragments, sb, ops); + } else { + buildSpannableFromFragmentsDuplicated(context, fragments, sb, ops); + } + } + + private static void buildSpannableFromFragmentsDuplicated( Context context, ReadableArray fragments, SpannableStringBuilder sb, @@ -152,8 +165,8 @@ public class TextLayoutManager { } ops.add( new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET + if (textAttributes.mFontStyle != ReactConstants.UNSET + || textAttributes.mFontWeight != ReactConstants.UNSET || textAttributes.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -197,6 +210,17 @@ public class TextLayoutManager { } } + private static void buildSpannableFromFragmentsUnified( + Context context, + ReadableArray fragments, + SpannableStringBuilder sb, + List ops) { + + final BridgeTextFragmentList textFragmentList = new BridgeTextFragmentList(fragments); + + TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops); + } + // public because both ReactTextViewManager and ReactTextInputManager need to use this public static Spannable getOrCreateSpannableForText( Context context, @@ -219,7 +243,7 @@ public class TextLayoutManager { // a new spannable will be wiped out List ops = new ArrayList<>(); - buildSpannableFromFragment(context, attributedString.getArray("fragments"), sb, ops); + buildSpannableFromFragments(context, attributedString.getArray("fragments"), sb, ops); // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. @@ -368,10 +392,10 @@ public class TextLayoutManager { int maximumNumberOfLines = paragraphAttributes.hasKey(MAXIMUM_NUMBER_OF_LINES_KEY) ? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY) - : UNSET; + : ReactConstants.UNSET; int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0 ? layout.getLineCount() : Math.min(maximumNumberOfLines, layout.getLineCount()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index 60ef5f2e28d..f6f0287db6d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -8,7 +8,6 @@ package com.facebook.react.views.text; import static com.facebook.react.config.ReactFeatureFlags.enableTextSpannableCache; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; import android.graphics.Color; @@ -29,12 +28,15 @@ import com.facebook.common.logging.FLog; import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; +import com.facebook.react.views.text.fragments.MapBufferTextFragmentList; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaMeasureMode; import com.facebook.yoga.YogaMeasureOutput; @@ -123,7 +125,16 @@ public class TextLayoutManagerMapBuffer { == LayoutDirection.RTL; } - private static void buildSpannableFromFragment( + private static void buildSpannableFromFragments( + Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) { + if (ReactFeatureFlags.enableSpannableBuildingUnification) { + buildSpannableFromFragmentsUnified(context, fragments, sb, ops); + } else { + buildSpannableFromFragmentsDuplicated(context, fragments, sb, ops); + } + } + + private static void buildSpannableFromFragmentsDuplicated( Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) { for (int i = 0, length = fragments.getCount(); i < length; i++) { @@ -172,8 +183,8 @@ public class TextLayoutManagerMapBuffer { } ops.add( new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET + if (textAttributes.mFontStyle != ReactConstants.UNSET + || textAttributes.mFontWeight != ReactConstants.UNSET || textAttributes.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -217,6 +228,14 @@ public class TextLayoutManagerMapBuffer { } } + private static void buildSpannableFromFragmentsUnified( + Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) { + + final MapBufferTextFragmentList textFragmentList = new MapBufferTextFragmentList(fragments); + + TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops); + } + // public because both ReactTextViewManager and ReactTextInputManager need to use this public static Spannable getOrCreateSpannableForText( Context context, @@ -260,7 +279,7 @@ public class TextLayoutManagerMapBuffer { // a new spannable will be wiped out List ops = new ArrayList<>(); - buildSpannableFromFragment(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops); + buildSpannableFromFragments(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops); // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. @@ -390,10 +409,10 @@ public class TextLayoutManagerMapBuffer { int maximumNumberOfLines = paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES) ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) - : UNSET; + : ReactConstants.UNSET; int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0 ? layout.getLineCount() : Math.min(maximumNumberOfLines, layout.getLineCount()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt new file mode 100644 index 00000000000..9d2b29c2a8a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt @@ -0,0 +1,280 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.content.Context +import android.graphics.Color +import android.text.* +import android.view.View +import com.facebook.react.common.ReactConstants +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.ReactAccessibilityDelegate +import com.facebook.react.views.text.fragments.TextFragmentList + +/** Utility methods for building [Spannable]s */ +internal object TextLayoutUtils { + private const val INLINE_VIEW_PLACEHOLDER = "0" + + @JvmStatic + fun buildSpannableFromTextFragmentList( + context: Context, + textFragmentList: TextFragmentList, + sb: SpannableStringBuilder, + ops: MutableList, + ) { + + for (i in 0 until textFragmentList.count) { + val fragment = textFragmentList.getFragment(i) + val start = sb.length + + // ReactRawText + val textAttributes = fragment.textAttributeProps + + addText(sb, fragment.string, textAttributes) + + val end = sb.length + val reactTag = if (fragment.hasReactTag()) fragment.reactTag else View.NO_ID + if (fragment.hasIsAttachment() && fragment.isAttachment) { + val width = PixelUtil.toPixelFromSP(fragment.width) + val height = PixelUtil.toPixelFromSP(fragment.height) + + addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height) + } else if (end >= start) { + addApplicableTextAttributeSpans(ops, textAttributes, reactTag, context, start, end) + } + } + } + + @JvmStatic + fun addText( + sb: SpannableStringBuilder, + text: String?, + textAttributeProvider: EffectiveTextAttributeProvider + ) { + sb.append(TextTransform.apply(text, textAttributeProvider.textTransform)) + } + + @JvmStatic + fun addInlineViewPlaceholderSpan( + ops: MutableList, + sb: SpannableStringBuilder, + reactTag: Int, + width: Float, + height: Float + ) { + ops.add( + SetSpanOperation( + sb.length - INLINE_VIEW_PLACEHOLDER.length, + sb.length, + TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt()))) + } + + @JvmStatic + fun addApplicableTextAttributeSpans( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + reactTag: Int, + context: Context, + start: Int, + end: Int + ) { + addColorSpanIfApplicable(ops, textAttributeProvider, start, end) + + addBackgroundColorSpanIfApplicable(ops, textAttributeProvider, start, end) + + addLinkSpanIfApplicable(ops, textAttributeProvider, reactTag, start, end) + + addLetterSpacingSpanIfApplicable(ops, textAttributeProvider, start, end) + + addFontSizeSpanIfApplicable(ops, textAttributeProvider, start, end) + + addCustomStyleSpanIfApplicable(ops, textAttributeProvider, context, start, end) + + addUnderlineSpanIfApplicable(ops, textAttributeProvider, start, end) + + addStrikethroughSpanIfApplicable(ops, textAttributeProvider, start, end) + + addShadowStyleSpanIfApplicable(ops, textAttributeProvider, start, end) + + addLineHeightSpanIfApplicable(ops, textAttributeProvider, start, end) + + addReactTagSpan(ops, start, end, reactTag) + } + + @JvmStatic + private fun addLinkSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + reactTag: Int, + start: Int, + end: Int + ) { + val roleIsLink = + textAttributeProvider.role?.let { it == ReactAccessibilityDelegate.Role.LINK } + ?: (textAttributeProvider.accessibilityRole == + ReactAccessibilityDelegate.AccessibilityRole.LINK) + if (roleIsLink) { + ops.add(SetSpanOperation(start, end, ReactClickableSpan(reactTag))) + } + } + + @JvmStatic + private fun addColorSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + if (textAttributeProvider.isColorSet) { + ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textAttributeProvider.color))) + } + } + + @JvmStatic + private fun addBackgroundColorSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + if (textAttributeProvider.isBackgroundColorSet) { + ops.add( + SetSpanOperation( + start, end, ReactBackgroundColorSpan(textAttributeProvider.backgroundColor))) + } + } + + @JvmStatic + private fun addLetterSpacingSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + val effectiveLetterSpacing = textAttributeProvider.effectiveLetterSpacing + + if (!effectiveLetterSpacing.isNaN()) { + ops.add(SetSpanOperation(start, end, CustomLetterSpacingSpan(effectiveLetterSpacing))) + } + } + + @JvmStatic + private fun addFontSizeSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + val effectiveFontSize = textAttributeProvider.effectiveFontSize + + if (effectiveFontSize != ReactConstants.UNSET) { + ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize))) + } + } + + @JvmStatic + private fun addCustomStyleSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + context: Context, + start: Int, + end: Int + ) { + val fontStyle = textAttributeProvider.fontStyle + val fontWeight = textAttributeProvider.fontWeight + val fontFamily = textAttributeProvider.fontFamily + + if (fontStyle != ReactConstants.UNSET || + fontWeight != ReactConstants.UNSET || + fontFamily != null) { + ops.add( + SetSpanOperation( + start, + end, + CustomStyleSpan( + fontStyle, + fontWeight, + textAttributeProvider.fontFeatureSettings, + fontFamily, + context.assets))) + } + } + + @JvmStatic + private fun addUnderlineSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + if (textAttributeProvider.isUnderlineTextDecorationSet) { + ops.add(SetSpanOperation(start, end, ReactUnderlineSpan())) + } + } + + @JvmStatic + private fun addStrikethroughSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + if (textAttributeProvider.isLineThroughTextDecorationSet) { + ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan())) + } + } + + @JvmStatic + private fun addShadowStyleSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + val hasTextShadowOffset = + textAttributeProvider.textShadowOffsetDx != 0f || + textAttributeProvider.textShadowOffsetDy != 0f + val hasTextShadowRadius = textAttributeProvider.textShadowRadius != 0f + val hasTextShadowColorAlpha = Color.alpha(textAttributeProvider.textShadowColor) != 0 + + if ((hasTextShadowOffset || hasTextShadowRadius) && hasTextShadowColorAlpha) { + ops.add( + SetSpanOperation( + start, + end, + ShadowStyleSpan( + textAttributeProvider.textShadowOffsetDx, + textAttributeProvider.textShadowOffsetDy, + textAttributeProvider.textShadowRadius, + textAttributeProvider.textShadowColor))) + } + } + + @JvmStatic + private fun addLineHeightSpanIfApplicable( + ops: MutableList, + textAttributeProvider: EffectiveTextAttributeProvider, + start: Int, + end: Int + ) { + val effectiveLineHeight = textAttributeProvider.effectiveLineHeight + if (!effectiveLineHeight.isNaN()) { + ops.add(SetSpanOperation(start, end, CustomLineHeightSpan(effectiveLineHeight))) + } + } + + @JvmStatic + private fun addReactTagSpan( + ops: MutableList, + start: Int, + end: Int, + reactTag: Int + ) { + ops.add(SetSpanOperation(start, end, ReactTagSpan(reactTag))) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java index ad96b7d4437..364b52401c8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import androidx.annotation.Nullable; import java.text.BreakIterator; /** Types of text transforms for CustomTextTransformSpan */ @@ -17,7 +18,7 @@ public enum TextTransform { CAPITALIZE, UNSET; - public static String apply(String text, TextTransform textTransform) { + public static String apply(@Nullable String text, TextTransform textTransform) { if (text == null) { return null; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt new file mode 100644 index 00000000000..53d1105e6ea --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ViewProps +import com.facebook.react.views.text.TextAttributeProps + +/** A [TextFragment] implementation backed by a a [ReadableMap] */ +internal class BridgeTextFragment(private val fragment: ReadableMap) : TextFragment { + override val textAttributeProps: TextAttributeProps + get() = + TextAttributeProps.fromReadableMap(ReactStylesDiffMap(fragment.getMap("textAttributes"))) + + override val string: String? + get() = fragment.getString("string") + + override fun hasReactTag(): Boolean = fragment.hasKey("reactTag") + + override val reactTag: Int + get() = fragment.getInt("reactTag") + + override fun hasIsAttachment(): Boolean = fragment.hasKey(ViewProps.IS_ATTACHMENT) + + override val isAttachment: Boolean + get() = fragment.getBoolean(ViewProps.IS_ATTACHMENT) + + override val width: Double + get() = fragment.getDouble(ViewProps.WIDTH) + + override val height: Double + get() = fragment.getDouble(ViewProps.HEIGHT) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt new file mode 100644 index 00000000000..898b91d965b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +import com.facebook.react.bridge.ReadableArray + +/** A list of [TextFragment]s backed by a [ReadableArray] */ +internal class BridgeTextFragmentList(private val fragments: ReadableArray) : TextFragmentList { + override fun getFragment(index: Int): TextFragment = BridgeTextFragment(fragments.getMap(index)) + + override val count: Int + get() = fragments.size() +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt new file mode 100644 index 00000000000..ccaf93478c6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +import com.facebook.react.common.mapbuffer.MapBuffer +import com.facebook.react.views.text.TextAttributeProps +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_HEIGHT +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_IS_ATTACHMENT +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_REACT_TAG +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_STRING +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_TEXT_ATTRIBUTES +import com.facebook.react.views.text.TextLayoutManagerMapBuffer.FR_KEY_WIDTH + +/** A [TextFragment] implementation backed by a [MapBuffer] */ +internal class MapBufferTextFragment(private val fragment: MapBuffer) : TextFragment { + override val textAttributeProps: TextAttributeProps + get() = TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES.toInt())) + + override val string: String + get() = fragment.getString(FR_KEY_STRING.toInt()) + + override fun hasReactTag(): Boolean = fragment.contains(FR_KEY_REACT_TAG.toInt()) + + override val reactTag: Int + get() = fragment.getInt(FR_KEY_REACT_TAG.toInt()) + + override fun hasIsAttachment(): Boolean = fragment.contains(FR_KEY_IS_ATTACHMENT.toInt()) + + override val isAttachment: Boolean + get() = fragment.getBoolean(FR_KEY_IS_ATTACHMENT.toInt()) + + override val width: Double + get() = fragment.getDouble(FR_KEY_WIDTH.toInt()) + + override val height: Double + get() = fragment.getDouble(FR_KEY_HEIGHT.toInt()) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt new file mode 100644 index 00000000000..d6816e04df9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +import com.facebook.react.common.mapbuffer.MapBuffer + +/** A list of [TextFragment]s backed by a [MapBuffer] */ +internal class MapBufferTextFragmentList(private val fragments: MapBuffer) : TextFragmentList { + override fun getFragment(index: Int): TextFragment = + MapBufferTextFragment(fragments.getMapBuffer(index)) + + override val count: Int + get() = fragments.count +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt new file mode 100644 index 00000000000..b1692809499 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +import com.facebook.react.views.text.TextAttributeProps + +/** Interface for a text fragment */ +internal interface TextFragment { + val textAttributeProps: TextAttributeProps + + val string: String? + + fun hasReactTag(): Boolean + + val reactTag: Int + + fun hasIsAttachment(): Boolean + + val isAttachment: Boolean + + val width: Double + + val height: Double +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt new file mode 100644 index 00000000000..84488da8448 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.fragments + +/** Interface for a list of [TextFragment]s */ +internal interface TextFragmentList { + fun getFragment(index: Int): TextFragment + + val count: Int +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 1c7950e4b3c..d70f9e67b3c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -46,6 +46,7 @@ import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.common.ReactConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.StateWrapper; @@ -96,8 +97,6 @@ public class ReactEditText extends AppCompatEditText { /** A count of events sent to JS or C++. */ protected int mNativeEventCount; - private static final int UNSET = -1; - private @Nullable ArrayList mListeners; private @Nullable TextWatcherDelegator mTextWatcherDelegator; private int mStagedInputType; @@ -114,8 +113,8 @@ public class ReactEditText extends AppCompatEditText { private TextAttributes mTextAttributes; private boolean mTypefaceDirty = false; private @Nullable String mFontFamily = null; - private int mFontWeight = UNSET; - private int mFontStyle = UNSET; + private int mFontWeight = ReactConstants.UNSET; + private int mFontStyle = ReactConstants.UNSET; private boolean mAutoFocus = false; private boolean mDidAttachToWindow = false; private @Nullable String mPlaceholder = null; @@ -379,7 +378,7 @@ public class ReactEditText extends AppCompatEditText { return; } - if (start != UNSET && end != UNSET) { + if (start != ReactConstants.UNSET && end != ReactConstants.UNSET) { // clamp selection values for safety start = clampToTextLength(start); end = clampToTextLength(end); @@ -588,8 +587,8 @@ public class ReactEditText extends AppCompatEditText { // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything // nonstandard - if (mFontStyle != UNSET - || mFontWeight != UNSET + if (mFontStyle != ReactConstants.UNSET + || mFontWeight != ReactConstants.UNSET || mFontFamily != null || getFontFeatureSettings() != null) { setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG); @@ -819,8 +818,8 @@ public class ReactEditText extends AppCompatEditText { new CustomLetterSpacingSpan(effectiveLetterSpacing), 0, workingText.length(), spanFlags); } - if (mFontStyle != UNSET - || mFontWeight != UNSET + if (mFontStyle != ReactConstants.UNSET + || mFontWeight != ReactConstants.UNSET || mFontFamily != null || getFontFeatureSettings() != null) { workingText.setSpan( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index 878da519917..9935359203a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -36,7 +36,7 @@ import com.facebook.yoga.YogaNode; public class ReactTextInputShadowNode extends ReactBaseTextShadowNode implements YogaMeasureFunction { - private int mMostRecentEventCount = UNSET; + private int mMostRecentEventCount = ReactConstants.UNSET; private @Nullable EditText mInternalEditText; private @Nullable ReactTextInputLocalData mLocalData; @@ -108,7 +108,7 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode } else { editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.getEffectiveFontSize()); - if (mNumberOfLines != UNSET) { + if (mNumberOfLines != ReactConstants.UNSET) { editText.setLines(mNumberOfLines); } @@ -191,7 +191,7 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { super.onCollectExtraUpdates(uiViewOperationQueue); - if (mMostRecentEventCount != UNSET) { + if (mMostRecentEventCount != ReactConstants.UNSET) { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( spannedFromShadowNode(