From 9dc2eca0946238c0103627221dfb4e2f19d5eb72 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 14:31:39 +0200 Subject: [PATCH 01/14] De-duplicate building `Spannable` from fragments ...between the variant which deserializes fragments from a `MapBuffer` and a variant which depends `on com.facebook.react.bridge.*` APIs --- .../react/views/text/TextLayoutManager.java | 101 +-------------- .../text/TextLayoutManagerMapBuffer.java | 103 +-------------- .../react/views/text/TextLayoutUtils.java | 122 ++++++++++++++++++ .../text/fragments/BridgeTextFragment.kt | 28 ++++ .../text/fragments/BridgeTextFragmentList.kt | 14 ++ .../text/fragments/MapBufferTextFragment.kt | 33 +++++ .../fragments/MapBufferTextFragmentList.kt | 14 ++ .../views/text/fragments/TextFragment.kt | 24 ++++ .../views/text/fragments/TextFragmentList.kt | 10 ++ 9 files changed, 257 insertions(+), 192 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java 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/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 93eda5d06b3866..61213a6a283a1c 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 @@ -10,7 +10,6 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; -import android.graphics.Color; import android.os.Build; import android.text.BoringLayout; import android.text.Layout; @@ -21,7 +20,6 @@ import android.text.TextPaint; import android.util.LayoutDirection; import android.util.LruCache; -import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -33,10 +31,8 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.common.build.ReactBuildConfig; 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,102 +95,15 @@ public static void deleteCachedSpannableForTag(int reactTag) { sTagToSpannableCache.remove(reactTag); } - private static void buildSpannableFromFragment( + private static void buildSpannableFromFragments( Context context, ReadableArray fragments, SpannableStringBuilder sb, List ops) { - for (int i = 0, length = fragments.size(); i < length; i++) { - ReadableMap fragment = fragments.getMap(i); - int start = sb.length(); - - // ReactRawText - TextAttributeProps textAttributes = - TextAttributeProps.fromReadableMap( - new ReactStylesDiffMap(fragment.getMap("textAttributes"))); - - sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform)); - - int end = sb.length(); - int reactTag = fragment.hasKey("reactTag") ? fragment.getInt("reactTag") : View.NO_ID; - if (fragment.hasKey(ViewProps.IS_ATTACHMENT) - && fragment.getBoolean(ViewProps.IS_ATTACHMENT)) { - float width = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.WIDTH)); - float height = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.HEIGHT)); - ops.add( - new SetSpanOperation( - sb.length() - INLINE_VIEW_PLACEHOLDER.length(), - sb.length(), - new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); - } else if (end >= start) { - boolean roleIsLink = - textAttributes.mRole != null - ? textAttributes.mRole == Role.LINK - : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; - if (roleIsLink) { - ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); - } - if (textAttributes.mIsColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactForegroundColorSpan(textAttributes.mColor))); - } - if (textAttributes.mIsBackgroundColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); - } - if (!Float.isNaN(textAttributes.getLetterSpacing())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); - } - ops.add( - new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET - || textAttributes.mFontFamily != null) { - ops.add( - new SetSpanOperation( - start, - end, - new CustomStyleSpan( - textAttributes.mFontStyle, - textAttributes.mFontWeight, - textAttributes.mFontFeatureSettings, - textAttributes.mFontFamily, - context.getAssets()))); - } - if (textAttributes.mIsUnderlineTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); - } - if (textAttributes.mIsLineThroughTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); - } - if ((textAttributes.mTextShadowOffsetDx != 0 - || textAttributes.mTextShadowOffsetDy != 0 - || textAttributes.mTextShadowRadius != 0) - && Color.alpha(textAttributes.mTextShadowColor) != 0) { - ops.add( - new SetSpanOperation( - start, - end, - new ShadowStyleSpan( - textAttributes.mTextShadowOffsetDx, - textAttributes.mTextShadowOffsetDy, - textAttributes.mTextShadowRadius, - textAttributes.mTextShadowColor))); - } - if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); - } + final var textFragmentList = new BridgeTextFragmentList(fragments); - ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); - } - } + TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops); } // public because both ReactTextViewManager and ReactTextInputManager need to use this @@ -219,7 +128,7 @@ private static Spannable createSpannableFromAttributedString( // a new spannable will be wiped out List ops = new ArrayList<>(); - buildSpannableFromFragment(context, attributedString.getArray("fragments"), sb, ops); + buildSpannableFromFragments(context, attributedString.getArray("fragments"), sb, ops); // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index 60ef5f2e28de9d..e1e34c9baf7d32 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 @@ -11,7 +11,6 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; -import android.graphics.Color; import android.os.Build; import android.text.BoringLayout; import android.text.Layout; @@ -22,7 +21,6 @@ import android.text.TextPaint; import android.util.LayoutDirection; import android.util.LruCache; -import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -33,8 +31,7 @@ import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; 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,98 +120,12 @@ public static boolean isRTL(MapBuffer attributedString) { == LayoutDirection.RTL; } - private static void buildSpannableFromFragment( - Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) { - - for (int i = 0, length = fragments.getCount(); i < length; i++) { - MapBuffer fragment = fragments.getMapBuffer(i); - int start = sb.length(); - - TextAttributeProps textAttributes = - TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES)); - - sb.append( - TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform)); - - int end = sb.length(); - int reactTag = - fragment.contains(FR_KEY_REACT_TAG) ? fragment.getInt(FR_KEY_REACT_TAG) : View.NO_ID; - if (fragment.contains(FR_KEY_IS_ATTACHMENT) && fragment.getBoolean(FR_KEY_IS_ATTACHMENT)) { - float width = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_WIDTH)); - float height = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_HEIGHT)); - ops.add( - new SetSpanOperation( - sb.length() - INLINE_VIEW_PLACEHOLDER.length(), - sb.length(), - new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); - } else if (end >= start) { - boolean roleIsLink = - textAttributes.mRole != null - ? textAttributes.mRole == Role.LINK - : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; - if (roleIsLink) { - ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); - } - if (textAttributes.mIsColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactForegroundColorSpan(textAttributes.mColor))); - } - if (textAttributes.mIsBackgroundColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); - } - if (!Float.isNaN(textAttributes.getLetterSpacing())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); - } - ops.add( - new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET - || textAttributes.mFontFamily != null) { - ops.add( - new SetSpanOperation( - start, - end, - new CustomStyleSpan( - textAttributes.mFontStyle, - textAttributes.mFontWeight, - textAttributes.mFontFeatureSettings, - textAttributes.mFontFamily, - context.getAssets()))); - } - if (textAttributes.mIsUnderlineTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); - } - if (textAttributes.mIsLineThroughTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); - } - if ((textAttributes.mTextShadowOffsetDx != 0 - || textAttributes.mTextShadowOffsetDy != 0 - || textAttributes.mTextShadowRadius != 0) - && Color.alpha(textAttributes.mTextShadowColor) != 0) { - ops.add( - new SetSpanOperation( - start, - end, - new ShadowStyleSpan( - textAttributes.mTextShadowOffsetDx, - textAttributes.mTextShadowOffsetDy, - textAttributes.mTextShadowRadius, - textAttributes.mTextShadowColor))); - } - if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); - } + private static void buildSpannableFromFragments(Context context, MapBuffer fragments, SpannableStringBuilder sb, + List ops) { - ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); - } - } + final var textFragmentList = new MapBufferTextFragmentList(fragments); + + TextLayoutUtils.buildSpannableFromTextFragmentList(context, textFragmentList, sb, ops); } // public because both ReactTextViewManager and ReactTextInputManager need to use this @@ -260,7 +171,7 @@ private static Spannable createSpannableFromAttributedString( // a new spannable will be wiped out List ops = new ArrayList<>(); - buildSpannableFromFragment(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops); + buildSpannableFromFragments(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops); // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java new file mode 100644 index 00000000000000..aa66b493560d48 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java @@ -0,0 +1,122 @@ +/* + * 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.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; +import com.facebook.react.views.text.fragments.TextFragment; +import com.facebook.react.views.text.fragments.TextFragmentList; + +import java.util.List; + +import static com.facebook.react.views.text.TextAttributeProps.UNSET; + +public class TextLayoutUtils { + private static final String INLINE_VIEW_PLACEHOLDER = "0"; + + public static void buildSpannableFromTextFragmentList( + Context context, + TextFragmentList textFragmentList, + SpannableStringBuilder sb, + List ops) { + + for (int i = 0, length = textFragmentList.getCount(); i < length; i++) { + final TextFragment fragment = textFragmentList.getFragment(i); + int start = sb.length(); + + // ReactRawText + TextAttributeProps textAttributes = fragment.getTextAttributeProps(); + + sb.append(TextTransform.apply(fragment.getString(), textAttributes.mTextTransform)); + + int end = sb.length(); + int reactTag = fragment.hasReactTag() ? fragment.getReactTag() : View.NO_ID; + if (fragment.hasIsAttachment() + && fragment.isAttachment()) { + float width = PixelUtil.toPixelFromSP(fragment.getWidth()); + float height = PixelUtil.toPixelFromSP(fragment.getHeight()); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + } else if (end >= start) { + boolean roleIsLink = + textAttributes.mRole != null + ? textAttributes.mRole == Role.LINK + : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { + ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); + } + if (textAttributes.mIsColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactForegroundColorSpan(textAttributes.mColor))); + } + if (textAttributes.mIsBackgroundColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); + } + if (!Float.isNaN(textAttributes.getLetterSpacing())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); + } + ops.add( + new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); + if (textAttributes.mFontStyle != UNSET + || textAttributes.mFontWeight != UNSET + || textAttributes.mFontFamily != null) { + ops.add( + new SetSpanOperation( + start, + end, + new CustomStyleSpan( + textAttributes.mFontStyle, + textAttributes.mFontWeight, + textAttributes.mFontFeatureSettings, + textAttributes.mFontFamily, + context.getAssets()))); + } + if (textAttributes.mIsUnderlineTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); + } + if (textAttributes.mIsLineThroughTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); + } + if ((textAttributes.mTextShadowOffsetDx != 0 + || textAttributes.mTextShadowOffsetDy != 0 + || textAttributes.mTextShadowRadius != 0) + && Color.alpha(textAttributes.mTextShadowColor) != 0) { + ops.add( + new SetSpanOperation( + start, + end, + new ShadowStyleSpan( + textAttributes.mTextShadowOffsetDx, + textAttributes.mTextShadowOffsetDy, + textAttributes.mTextShadowRadius, + textAttributes.mTextShadowColor))); + } + if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); + } + + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + } + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt new file mode 100644 index 00000000000000..39a7587997ccd8 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragment.kt @@ -0,0 +1,28 @@ +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 mFragment: ReadableMap) : TextFragment { + override fun getTextAttributeProps(): TextAttributeProps = + TextAttributeProps.fromReadableMap(ReactStylesDiffMap(mFragment.getMap("textAttributes"))) + + override fun getString(): String? = mFragment.getString("string") + + override fun hasReactTag(): Boolean = mFragment.hasKey("reactTag") + + override fun getReactTag(): Int = mFragment.getInt("reactTag") + + override fun hasIsAttachment(): Boolean = mFragment.hasKey(ViewProps.IS_ATTACHMENT) + + override fun isAttachment(): Boolean = mFragment.getBoolean(ViewProps.IS_ATTACHMENT) + + override fun getWidth(): Double = mFragment.getDouble(ViewProps.WIDTH) + + override fun getHeight(): Double = mFragment.getDouble(ViewProps.HEIGHT) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt new file mode 100644 index 00000000000000..8c1bab6c389108 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/BridgeTextFragmentList.kt @@ -0,0 +1,14 @@ +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 mFragments: ReadableArray) : TextFragmentList { + + override fun getFragment(index: Int): TextFragment = BridgeTextFragment(mFragments.getMap(index)) + + override val count: Int + get() = mFragments.size() +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt new file mode 100644 index 00000000000000..2709e89b896bf7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt @@ -0,0 +1,33 @@ +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 fun getTextAttributeProps(): TextAttributeProps = + TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES.toInt())) + + override fun getString(): String = fragment.getString(FR_KEY_STRING.toInt()) + + override fun hasReactTag(): Boolean = fragment.contains(FR_KEY_REACT_TAG.toInt()) + + override fun getReactTag(): Int = fragment.getInt(FR_KEY_REACT_TAG.toInt()) + + override fun hasIsAttachment(): Boolean = fragment.contains(FR_KEY_IS_ATTACHMENT.toInt()) + + override fun isAttachment(): Boolean = fragment.getBoolean(FR_KEY_IS_ATTACHMENT.toInt()) + + override fun getWidth(): Double = fragment.getDouble(FR_KEY_WIDTH.toInt()) + + override fun getHeight(): Double = fragment.getDouble(FR_KEY_HEIGHT.toInt()) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt new file mode 100644 index 00000000000000..82365340442885 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragmentList.kt @@ -0,0 +1,14 @@ +package com.facebook.react.views.text.fragments + +import com.facebook.react.common.mapbuffer.MapBuffer + +/** + * A list of [TextFragment]s backed by a [MapBuffer] + */ +internal class MapBufferTextFragmentList(private val mFragments: MapBuffer) : TextFragmentList { + override fun getFragment(index: Int): TextFragment = + MapBufferTextFragment(mFragments.getMapBuffer(index)) + + override val count: Int + get() = mFragments.count +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt new file mode 100644 index 00000000000000..68e95e2d2ac014 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt @@ -0,0 +1,24 @@ +package com.facebook.react.views.text.fragments + +import com.facebook.react.views.text.TextAttributeProps + +/** + * Interface for a text fragment + */ +internal interface TextFragment { + fun getTextAttributeProps(): TextAttributeProps + + fun getString(): String? + + fun hasReactTag(): Boolean + + fun getReactTag(): Int + + fun hasIsAttachment(): Boolean + + fun isAttachment(): Boolean + + fun getWidth(): Double + + fun getHeight(): Double +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt new file mode 100644 index 00000000000000..b50ed43cb646e3 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragmentList.kt @@ -0,0 +1,10 @@ +package com.facebook.react.views.text.fragments + +/** + * Interface for a list of [TextFragment]s + */ +internal interface TextFragmentList { + fun getFragment(index: Int): TextFragment + + val count: Int +} From e35564b345b88c5ea731018ac45ed7bb068f7622 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 15:53:21 +0200 Subject: [PATCH 02/14] De-duplicate building `Spannable` ...between the variant that builds it from fragments and the on which builds it from the shadow tree --- .../text/EffectiveTextAttributeProvider.java | 47 ++++ .../views/text/ReactBaseTextShadowNode.java | 232 ++++++++++++------ .../react/views/text/SetSpanOperation.java | 2 +- .../react/views/text/TextAttributeProps.java | 91 ++++++- .../react/views/text/TextLayoutUtils.java | 226 ++++++++++------- 5 files changed, 438 insertions(+), 160 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java new file mode 100644 index 00000000000000..1d83ce001bce0d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java @@ -0,0 +1,47 @@ +package com.facebook.react.views.text; + +import com.facebook.react.uimanager.ReactAccessibilityDelegate; +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; + +/** + * Interface for an entity providing effective text attributes of a text node/fragment + */ +public interface EffectiveTextAttributeProvider { + TextTransform getTextTransform(); + + float getEffectiveLetterSpacing(); + + Role getRole(); + + ReactAccessibilityDelegate.AccessibilityRole getAccessibilityRole(); + + boolean isBackgroundColorSet(); + + int getBackgroundColor(); + + boolean isColorSet(); + + int getColor(); + + int getFontStyle(); + + int getFontWeight(); + + String getFontFamily(); + + String getFontFeatureSettings(); + + boolean isUnderlineTextDecorationSet(); + + boolean isLineThroughTextDecorationSet(); + + float getTextShadowOffsetDx(); + + float getTextShadowOffsetDy(); + + float getTextShadowRadius(); + + int getTextShadowColor(); + + float getEffectiveLineHeight(); +} 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 cb02e705da57ae..b13cf19aeb9574 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 @@ -7,7 +7,6 @@ package com.facebook.react.views.text; -import android.graphics.Color; import android.graphics.Typeface; import android.os.Build; import android.text.Layout; @@ -50,6 +49,135 @@ * can be used in concrete classes to feed native views and compute layout. */ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { + /** + * Implementation of {@link EffectiveTextAttributeProvider} that provides effective text + * attributes based on a {@link ReactBaseTextShadowNode} instance and its parent. + */ + private static class HierarchicTextAttributeProvider implements EffectiveTextAttributeProvider { + final private ReactBaseTextShadowNode textShadowNode; + final private TextAttributes parentTextAttributes; + final private TextAttributes textAttributes; + + private HierarchicTextAttributeProvider( + ReactBaseTextShadowNode textShadowNode, + TextAttributes parentTextAttributes, + TextAttributes textAttributes + ) { + this.textShadowNode = textShadowNode; + this.parentTextAttributes = parentTextAttributes; + this.textAttributes = textAttributes; + } + + @Override + public TextTransform getTextTransform() { + return textAttributes.getTextTransform(); + } + + @Override + public Role getRole() { + return textShadowNode.mRole; + } + + @Override + public AccessibilityRole getAccessibilityRole() { + return textShadowNode.mAccessibilityRole; + } + + @Override + public boolean isBackgroundColorSet() { + return textShadowNode.mIsBackgroundColorSet; + } + + @Override + public int getBackgroundColor() { + return textShadowNode.mBackgroundColor; + } + + @Override + public boolean isColorSet() { + return textShadowNode.mIsColorSet; + } + + @Override + public int getColor() { + return textShadowNode.mColor; + } + + @Override + public int getFontStyle() { + return textShadowNode.mFontStyle; + } + + @Override + public int getFontWeight() { + return textShadowNode.mFontWeight; + } + + @Override + public String getFontFamily() { + return textShadowNode.mFontFamily; + } + + @Override + public String getFontFeatureSettings() { + return textShadowNode.mFontFeatureSettings; + } + + @Override + public boolean isUnderlineTextDecorationSet() { + return textShadowNode.mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return textShadowNode.mIsLineThroughTextDecorationSet; + } + + @Override + public float getTextShadowOffsetDx() { + return textShadowNode.mTextShadowOffsetDx; + } + + @Override + public float getTextShadowOffsetDy() { + return textShadowNode.mTextShadowOffsetDy; + } + + @Override + public float getTextShadowRadius() { + return textShadowNode.mTextShadowRadius; + } + + @Override + public int getTextShadowColor() { + return textShadowNode.mTextShadowColor; + } + + @Override + public float getEffectiveLetterSpacing() { + final float letterSpacing = textAttributes.getEffectiveLetterSpacing(); + + if (!Float.isNaN(letterSpacing) + && (parentTextAttributes == null + || parentTextAttributes.getEffectiveLetterSpacing() != letterSpacing)) { + return letterSpacing; + } else { + return Float.NaN; + } + } + + @Override + public float getEffectiveLineHeight() { + final float lineHeight = textAttributes.getEffectiveLineHeight(); + if (!Float.isNaN(lineHeight) + && (parentTextAttributes == null + || parentTextAttributes.getEffectiveLineHeight() != lineHeight)) { + return lineHeight; + } else { + return Float.NaN; + } + } + } // Use a direction weak character so the placeholder doesn't change the direction of the previous // character. @@ -85,13 +213,13 @@ private static void buildSpannedFromShadowNode( textAttributes = textShadowNode.mTextAttributes; } + final var textAttributeProvider = new HierarchicTextAttributeProvider(textShadowNode, parentTextAttributes, textAttributes); + for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) { ReactShadowNode child = textShadowNode.getChildAt(i); if (child instanceof ReactRawTextShadowNode) { - sb.append( - TextTransform.apply( - ((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform())); + TextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider); } else if (child instanceof ReactBaseTextShadowNode) { buildSpannedFromShadowNode( (ReactBaseTextShadowNode) child, @@ -132,11 +260,9 @@ private static void buildSpannedFromShadowNode( // 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))); + + TextLayoutUtils.addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); + inlineViews.put(reactTag, child); } else { throw new IllegalViewOperationException( @@ -146,78 +272,34 @@ private static void buildSpannedFromShadowNode( } 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))); - } + final int reactTag = textShadowNode.getReactTag(); + + TextLayoutUtils.addColorSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addBackgroundColorSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addLinkSpanIfApplicable(ops, textAttributeProvider, reactTag, start, end); + + TextLayoutUtils.addLetterSpacingSpanIfApplicable(ops, textAttributeProvider, start, end); + 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 != UNSET - || textShadowNode.mFontWeight != 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())); + TextLayoutUtils.addFontSizeSpan(ops, start, end, effectiveFontSize); } - 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()))); + TextLayoutUtils.addCustomStyleSpanIfApplicable(ops, textAttributeProvider, textShadowNode.getThemedContext(), start, end); + + TextLayoutUtils.addUnderlineSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addStrikethroughSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addShadowStyleSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addLineHeightSpanIfApplicable(ops, textAttributeProvider, start, end); + + TextLayoutUtils.addReactTagSpan(ops, start, end, reactTag); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java index 5df16e4e283822..57649508d54316 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SetSpanOperation.java @@ -12,7 +12,7 @@ import android.text.Spanned; import com.facebook.common.logging.FLog; -class SetSpanOperation { +public class SetSpanOperation { private static final String TAG = "SetSpanOperation"; static final int SPAN_MAX_PRIORITY = Spanned.SPAN_PRIORITY >> Spanned.SPAN_PRIORITY_SHIFT; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index aa450d79fd2111..dc768a085c1478 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 @@ -30,7 +30,7 @@ // TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents // TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to // TextAttributeProps (e.g. TextAlign) -public class TextAttributeProps { +public class TextAttributeProps implements EffectiveTextAttributeProvider { // constants for Text Attributes serialization public static final short TA_KEY_FOREGROUND_COLOR = 0; @@ -343,6 +343,7 @@ private static float getFloatProp(ReactStylesDiffMap mProps, String name, float // Returns a line height which takes into account the requested line height // and the height of the inline images. + @Override public float getEffectiveLineHeight() { boolean useInlineViewHeight = !Float.isNaN(mLineHeight) @@ -371,7 +372,13 @@ private void setLetterSpacing(float letterSpacing) { mLetterSpacingInput = letterSpacing; } - public float getLetterSpacing() { + @Override + public TextTransform getTextTransform() { + return mTextTransform; + } + + @Override + public float getEffectiveLetterSpacing() { float letterSpacingPixels = mAllowFontScaling ? PixelUtil.toPixelFromSP(mLetterSpacingInput) @@ -679,4 +686,84 @@ public static int getHyphenationFrequency(@Nullable String hyphenationFrequency) } return androidHyphenationFrequency; } + + @Override + public Role getRole() { + return mRole; + } + + @Override + public AccessibilityRole getAccessibilityRole() { + return mAccessibilityRole; + } + + @Override + public boolean isBackgroundColorSet() { + return mIsBackgroundColorSet; + } + + @Override + public int getBackgroundColor() { + return mBackgroundColor; + } + + @Override + public boolean isColorSet() { + return mIsColorSet; + } + + @Override + public int getColor() { + return mColor; + } + + @Override + public int getFontStyle() { + return mFontStyle; + } + + @Override + public int getFontWeight() { + return mFontWeight; + } + + @Override + public String getFontFamily() { + return mFontFamily; + } + + @Override + public String getFontFeatureSettings() { + return mFontFeatureSettings; + } + + @Override + public boolean isUnderlineTextDecorationSet() { + return mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return mIsLineThroughTextDecorationSet; + } + + @Override + public float getTextShadowOffsetDx() { + return mTextShadowOffsetDx; + } + + @Override + public float getTextShadowOffsetDy() { + return mTextShadowOffsetDy; + } + + @Override + public float getTextShadowRadius() { + return mTextShadowRadius; + } + + @Override + public int getTextShadowColor() { + return mTextShadowColor; + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java index aa66b493560d48..eeb492b752638f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java @@ -11,24 +11,23 @@ import android.graphics.Color; import android.text.*; import android.view.View; +import com.facebook.react.common.assets.ReactFontManager; 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.ReactAccessibilityDelegate; import com.facebook.react.views.text.fragments.TextFragment; import com.facebook.react.views.text.fragments.TextFragmentList; import java.util.List; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - +/** + * Class containing static methods for building {@link Spannable}s + */ public class TextLayoutUtils { private static final String INLINE_VIEW_PLACEHOLDER = "0"; + private static final int UNSET = ReactFontManager.TypefaceStyle.UNSET; - public static void buildSpannableFromTextFragmentList( - Context context, - TextFragmentList textFragmentList, - SpannableStringBuilder sb, - List ops) { + public static void buildSpannableFromTextFragmentList(Context context, TextFragmentList textFragmentList, + SpannableStringBuilder sb, List ops) { for (int i = 0, length = textFragmentList.getCount(); i < length; i++) { final TextFragment fragment = textFragmentList.getFragment(i); @@ -37,86 +36,149 @@ public static void buildSpannableFromTextFragmentList( // ReactRawText TextAttributeProps textAttributes = fragment.getTextAttributeProps(); - sb.append(TextTransform.apply(fragment.getString(), textAttributes.mTextTransform)); + TextLayoutUtils.addText(sb, fragment.getString(), textAttributes); int end = sb.length(); int reactTag = fragment.hasReactTag() ? fragment.getReactTag() : View.NO_ID; - if (fragment.hasIsAttachment() - && fragment.isAttachment()) { + if (fragment.hasIsAttachment() && fragment.isAttachment()) { float width = PixelUtil.toPixelFromSP(fragment.getWidth()); float height = PixelUtil.toPixelFromSP(fragment.getHeight()); - ops.add( - new SetSpanOperation( - sb.length() - INLINE_VIEW_PLACEHOLDER.length(), - sb.length(), - new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + + addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); } else if (end >= start) { - boolean roleIsLink = - textAttributes.mRole != null - ? textAttributes.mRole == Role.LINK - : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; - if (roleIsLink) { - ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); - } - if (textAttributes.mIsColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactForegroundColorSpan(textAttributes.mColor))); - } - if (textAttributes.mIsBackgroundColorSet) { - ops.add( - new SetSpanOperation( - start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); - } - if (!Float.isNaN(textAttributes.getLetterSpacing())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); - } - ops.add( - new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET - || textAttributes.mFontFamily != null) { - ops.add( - new SetSpanOperation( - start, - end, - new CustomStyleSpan( - textAttributes.mFontStyle, - textAttributes.mFontWeight, - textAttributes.mFontFeatureSettings, - textAttributes.mFontFamily, - context.getAssets()))); - } - if (textAttributes.mIsUnderlineTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); - } - if (textAttributes.mIsLineThroughTextDecorationSet) { - ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); - } - if ((textAttributes.mTextShadowOffsetDx != 0 - || textAttributes.mTextShadowOffsetDy != 0 - || textAttributes.mTextShadowRadius != 0) - && Color.alpha(textAttributes.mTextShadowColor) != 0) { - ops.add( - new SetSpanOperation( - start, - end, - new ShadowStyleSpan( - textAttributes.mTextShadowOffsetDx, - textAttributes.mTextShadowOffsetDy, - textAttributes.mTextShadowRadius, - textAttributes.mTextShadowColor))); - } - if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { - ops.add( - new SetSpanOperation( - start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); - } - - ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + addLinkSpanIfApplicable(ops, textAttributes, reactTag, start, end); + + addColorSpanIfApplicable(ops, textAttributes, start, end); + + addBackgroundColorSpanIfApplicable(ops, textAttributes, start, end); + + addLetterSpacingSpanIfApplicable(ops, textAttributes, start, end); + + addFontSizeSpan(ops, start, end, textAttributes.mFontSize); + + addCustomStyleSpanIfApplicable(ops, textAttributes, context, start, end); + + addUnderlineSpanIfApplicable(ops, textAttributes, start, end); + + addStrikethroughSpanIfApplicable(ops, textAttributes, start, end); + + addShadowStyleSpanIfApplicable(ops, textAttributes, start, end); + + addLineHeightSpanIfApplicable(ops, textAttributes, start, end); + + addReactTagSpan(ops, start, end, reactTag); } } } + + public static void addText(SpannableStringBuilder sb, String text, + EffectiveTextAttributeProvider textAttributeProvider) { + sb.append(TextTransform.apply(text, textAttributeProvider.getTextTransform())); + } + + public static void addInlineViewPlaceholderSpan(List ops, SpannableStringBuilder sb, int reactTag + , float width, float height) { + ops.add(new SetSpanOperation(sb.length() - INLINE_VIEW_PLACEHOLDER.length(), sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + } + + + public static void addLinkSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int reactTag, + int start, int end) { + boolean roleIsLink = textAttributeProvider.getRole() != null ? + textAttributeProvider.getRole() == ReactAccessibilityDelegate.Role.LINK : + textAttributeProvider.getAccessibilityRole() == ReactAccessibilityDelegate.AccessibilityRole.LINK; + if (roleIsLink) { + ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); + } + } + + public static void addColorSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start, + int end) { + if (textAttributeProvider.isColorSet()) { + ops.add(new SetSpanOperation(start, end, new ReactForegroundColorSpan(textAttributeProvider.getColor()))); + } + } + + public static void addBackgroundColorSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, + int start, int end) { + if (textAttributeProvider.isBackgroundColorSet()) { + ops.add(new SetSpanOperation(start, end, + new ReactBackgroundColorSpan(textAttributeProvider.getBackgroundColor()))); + } + } + + public static void addLetterSpacingSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start + , int end) { + final float effectiveLetterSpacing = textAttributeProvider.getEffectiveLetterSpacing(); + + if (!Float.isNaN(effectiveLetterSpacing)) { + ops.add(new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing))); + } + } + + + public static void addFontSizeSpan(List ops, int start, int end, int effectiveFontSize) { + + ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); + } + + public static void addCustomStyleSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, + Context context, int start, int end) { + final int fontStyle = textAttributeProvider.getFontStyle(); + final int fontWeight = textAttributeProvider.getFontWeight(); + final String fontFamily = textAttributeProvider.getFontFamily(); + + if (fontStyle != UNSET || fontWeight != UNSET || fontFamily != null) { + ops.add(new SetSpanOperation(start, end, new CustomStyleSpan(fontStyle, + fontWeight, textAttributeProvider.getFontFeatureSettings(), + fontFamily, context.getAssets()))); + } + } + + public static void addUnderlineSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start, + int end) { + if (textAttributeProvider.isUnderlineTextDecorationSet()) { + ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); + } + } + + public static void addStrikethroughSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start + , int end) { + if (textAttributeProvider.isLineThroughTextDecorationSet()) { + ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); + } + } + + + public static void addShadowStyleSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start, + int end) { + if ((textAttributeProvider.getTextShadowOffsetDx() != 0 || textAttributeProvider.getTextShadowOffsetDy() != 0 || textAttributeProvider.getTextShadowRadius() != 0) && Color.alpha(textAttributeProvider.getTextShadowColor()) != 0) { + ops.add(new SetSpanOperation(start, end, new ShadowStyleSpan(textAttributeProvider.getTextShadowOffsetDx(), + textAttributeProvider.getTextShadowOffsetDy(), textAttributeProvider.getTextShadowRadius(), + textAttributeProvider.getTextShadowColor()))); + } + } + + public static void addLineHeightSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start, + int end) { + final float effectiveLineHeight = textAttributeProvider.getEffectiveLineHeight(); + if (!Float.isNaN(effectiveLineHeight)) { + ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight))); + } + } + + public static void addReactTagSpan(List ops, int start, int end, int reactTag) { + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + + } } From c3f0172741de467b6282d708b4e32a201a403d9b Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 16:50:08 +0200 Subject: [PATCH 03/14] Introduce an abstraction for getting the effective font size ...which further de-duplicates the `Spannable`-building code --- .../text/EffectiveTextAttributeProvider.java | 8 +++++++ .../views/text/ReactBaseTextShadowNode.java | 23 +++++++++++++------ .../react/views/text/TextAttributeProps.java | 5 ++++ .../react/views/text/TextLayoutUtils.java | 11 ++++++--- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java index 1d83ce001bce0d..2f4788c66ab1d6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java @@ -1,5 +1,6 @@ package com.facebook.react.views.text; +import com.facebook.react.common.assets.ReactFontManager; import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; @@ -7,10 +8,17 @@ * Interface for an entity providing effective text attributes of a text node/fragment */ public interface EffectiveTextAttributeProvider { + int UNSET = ReactFontManager.TypefaceStyle.UNSET; + TextTransform getTextTransform(); float getEffectiveLetterSpacing(); + /** + * @return The effective font size, or {@link #UNSET} if not set + */ + int getEffectiveFontSize(); + Role getRole(); ReactAccessibilityDelegate.AccessibilityRole getAccessibilityRole(); 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 b13cf19aeb9574..92f4cd47aee85d 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 @@ -166,6 +166,20 @@ public float getEffectiveLetterSpacing() { } } + @Override + public int getEffectiveFontSize() { + final int fontSize = textAttributes.getEffectiveFontSize(); + + if ( + // `getEffectiveFontSize` always returns a value so don't need to check for anything like `Float.NaN`. + parentTextAttributes == null + || parentTextAttributes.getEffectiveFontSize() != fontSize) { + return fontSize; + } else { + return UNSET; + } + } + @Override public float getEffectiveLineHeight() { final float lineHeight = textAttributes.getEffectiveLineHeight(); @@ -282,13 +296,8 @@ private static void buildSpannedFromShadowNode( TextLayoutUtils.addLetterSpacingSpanIfApplicable(ops, textAttributeProvider, start, end); - 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) { - TextLayoutUtils.addFontSizeSpan(ops, start, end, effectiveFontSize); - } + TextLayoutUtils.addFontSizeSpanIfApplicable(ops, textAttributeProvider, start, end); + TextLayoutUtils.addCustomStyleSpanIfApplicable(ops, textAttributeProvider, textShadowNode.getThemedContext(), start, end); TextLayoutUtils.addUnderlineSpanIfApplicable(ops, textAttributeProvider, start, end); 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 dc768a085c1478..67a8b77a49520d 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 @@ -393,6 +393,11 @@ public float getEffectiveLetterSpacing() { return letterSpacingPixels / mFontSize; } + @Override + public int getEffectiveFontSize() { + return mFontSize; + } + private void setAllowFontScaling(boolean allowFontScaling) { if (allowFontScaling != mAllowFontScaling) { mAllowFontScaling = allowFontScaling; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java index eeb492b752638f..dc82987b5b64a4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java @@ -54,7 +54,7 @@ public static void buildSpannableFromTextFragmentList(Context context, TextFragm addLetterSpacingSpanIfApplicable(ops, textAttributes, start, end); - addFontSizeSpan(ops, start, end, textAttributes.mFontSize); + addFontSizeSpanIfApplicable(ops, textAttributes, start, end); addCustomStyleSpanIfApplicable(ops, textAttributes, context, start, end); @@ -122,9 +122,14 @@ public static void addLetterSpacingSpanIfApplicable(List ops, } - public static void addFontSizeSpan(List ops, int start, int end, int effectiveFontSize) { + public static void addFontSizeSpanIfApplicable(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int start, + int end) { + final int effectiveFontSize = textAttributeProvider.getEffectiveFontSize(); - ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); + if (effectiveFontSize != UNSET) { + ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); + } } public static void addCustomStyleSpanIfApplicable(List ops, From df0ae3dea13999143f6bc03f310d7f055d02d555 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 14 Oct 2023 14:28:05 +0200 Subject: [PATCH 04/14] Port `EffectiveTextAttributeProvider` to Kotlin --- .../text/EffectiveTextAttributeProvider.java | 55 ------------------ .../text/EffectiveTextAttributeProvider.kt | 57 +++++++++++++++++++ .../views/text/ReactBaseTextShadowNode.java | 3 + .../react/views/text/TextAttributeProps.java | 4 ++ .../react/views/text/TextAttributes.java | 5 +- .../react/views/text/TextTransform.java | 3 +- 6 files changed, 70 insertions(+), 57 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java deleted file mode 100644 index 2f4788c66ab1d6..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.facebook.react.views.text; - -import com.facebook.react.common.assets.ReactFontManager; -import com.facebook.react.uimanager.ReactAccessibilityDelegate; -import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role; - -/** - * Interface for an entity providing effective text attributes of a text node/fragment - */ -public interface EffectiveTextAttributeProvider { - int UNSET = ReactFontManager.TypefaceStyle.UNSET; - - TextTransform getTextTransform(); - - float getEffectiveLetterSpacing(); - - /** - * @return The effective font size, or {@link #UNSET} if not set - */ - int getEffectiveFontSize(); - - Role getRole(); - - ReactAccessibilityDelegate.AccessibilityRole getAccessibilityRole(); - - boolean isBackgroundColorSet(); - - int getBackgroundColor(); - - boolean isColorSet(); - - int getColor(); - - int getFontStyle(); - - int getFontWeight(); - - String getFontFamily(); - - String getFontFeatureSettings(); - - boolean isUnderlineTextDecorationSet(); - - boolean isLineThroughTextDecorationSet(); - - float getTextShadowOffsetDx(); - - float getTextShadowOffsetDy(); - - float getTextShadowRadius(); - - int getTextShadowColor(); - - float getEffectiveLineHeight(); -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt new file mode 100644 index 00000000000000..fe71b17de734cd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt @@ -0,0 +1,57 @@ +package com.facebook.react.views.text + +import com.facebook.react.common.assets.ReactFontManager +import com.facebook.react.uimanager.ReactAccessibilityDelegate +import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role + +/** + * Interface for an entity providing effective text attributes of a text node/fragment + */ +internal interface EffectiveTextAttributeProvider { + companion object { + const val UNSET = ReactFontManager.TypefaceStyle.UNSET + } + + val textTransform: TextTransform + + val effectiveLetterSpacing: Float + + /** + * @return The effective font size, or [UNSET] if not set + */ + val effectiveFontSize: Int + + val role: 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 + + val effectiveLineHeight: Float +} 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 92f4cd47aee85d..52f03fd6ec0abe 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 @@ -14,6 +14,7 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.Gravity; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -69,10 +70,12 @@ private HierarchicTextAttributeProvider( } @Override + @NonNull public TextTransform getTextTransform() { return textAttributes.getTextTransform(); } + @Nullable @Override public Role getRole() { return textShadowNode.mRole; 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 67a8b77a49520d..1f4f9bfa88374d 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.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; @@ -94,6 +95,7 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { // `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available. protected int mLayoutDirection = UNSET; + @NonNull protected TextTransform mTextTransform = TextTransform.NONE; protected float mTextShadowOffsetDx = 0; @@ -373,6 +375,7 @@ private void setLetterSpacing(float letterSpacing) { } @Override + @NonNull public TextTransform getTextTransform() { return mTextTransform; } @@ -692,6 +695,7 @@ public static int getHyphenationFrequency(@Nullable String hyphenationFrequency) return androidHyphenationFrequency; } + @Nullable @Override public Role getRole() { return mRole; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java index 8d54a23bbdf6ac..e944d4fb7586e0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import androidx.annotation.NonNull; import com.facebook.common.logging.FLog; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.PixelUtil; @@ -29,6 +30,8 @@ public class TextAttributes { private float mLetterSpacing = Float.NaN; private float mMaxFontSizeMultiplier = Float.NaN; private float mHeightOfTallestInlineViewOrImage = Float.NaN; + + @NonNull private TextTransform mTextTransform = TextTransform.UNSET; public TextAttributes() {} @@ -118,7 +121,7 @@ public TextTransform getTextTransform() { return mTextTransform; } - public void setTextTransform(TextTransform textTransform) { + public void setTextTransform(@NonNull TextTransform textTransform) { mTextTransform = textTransform; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java index ad96b7d4437b76..e4f1f37bfd9740 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java @@ -8,6 +8,7 @@ package com.facebook.react.views.text; import java.text.BreakIterator; +import androidx.annotation.Nullable; /** Types of text transforms for CustomTextTransformSpan */ public enum TextTransform { @@ -17,7 +18,7 @@ public enum TextTransform { CAPITALIZE, UNSET; - public static String apply(String text, TextTransform textTransform) { + public static String apply(@Nullable String text, TextTransform textTransform) { if (text == null) { return null; } From 0c2c08b77eb34f6138be9b4c78c122f853bca5d8 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 14 Oct 2023 15:25:56 +0200 Subject: [PATCH 05/14] Port `HierarchicTextAttributeProvider` to Koltin --- .../views/text/BasicTextAttributeProvider.kt | 41 ++++ .../text/EffectiveTextAttributeProvider.kt | 36 +-- .../text/HierarchicTextAttributeProvider.kt | 52 ++++ .../views/text/ReactBaseTextShadowNode.java | 231 +++++++----------- 4 files changed, 178 insertions(+), 182 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/HierarchicTextAttributeProvider.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt new file mode 100644 index 00000000000000..54b6591ebaf3da --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt @@ -0,0 +1,41 @@ +package com.facebook.react.views.text + +import com.facebook.react.uimanager.ReactAccessibilityDelegate + +/** + * Interface for an entity providing basic text attributes of a text node/fragment. "Basic" means + * that they can be provided trivially, without processing the parent element. + */ +internal interface BasicTextAttributeProvider { + val role: ReactAccessibilityDelegate.Role? + + val accessibilityRole: ReactAccessibilityDelegate.AccessibilityRole? + + val isBackgroundColorSet: Boolean + + val backgroundColor: Int + + val isColorSet: Boolean + + val color: Int + + val fontStyle: Int + + val fontWeight: Int + + val fontFamily: String? + + val fontFeatureSettings: String? + + val isUnderlineTextDecorationSet: Boolean + + val isLineThroughTextDecorationSet: Boolean + + val textShadowOffsetDx: Float + + val textShadowOffsetDy: Float + + val textShadowRadius: Float + + val textShadowColor: Int +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt index fe71b17de734cd..6da9be9759663f 100644 --- 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 @@ -1,13 +1,11 @@ package com.facebook.react.views.text import com.facebook.react.common.assets.ReactFontManager -import com.facebook.react.uimanager.ReactAccessibilityDelegate -import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role /** * Interface for an entity providing effective text attributes of a text node/fragment */ -internal interface EffectiveTextAttributeProvider { +internal interface EffectiveTextAttributeProvider : BasicTextAttributeProvider { companion object { const val UNSET = ReactFontManager.TypefaceStyle.UNSET } @@ -21,37 +19,5 @@ internal interface EffectiveTextAttributeProvider { */ val effectiveFontSize: Int - val role: 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 - val effectiveLineHeight: Float } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt new file mode 100644 index 00000000000000..505d6f51eb0f0a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/HierarchicTextAttributeProvider.kt @@ -0,0 +1,52 @@ +package com.facebook.react.views.text + +/** + * 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 { + EffectiveTextAttributeProvider.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 52f03fd6ec0abe..7dd15c0c457837 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 @@ -14,7 +14,6 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.Gravity; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -22,6 +21,9 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.assets.ReactFontManager; +import com.facebook.react.internal.views.text.BasicTextAttributeProvider; +import com.facebook.react.internal.views.text.HierarchicTextAttributeProvider; +import com.facebook.react.internal.views.text.TextLayoutUtils; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; @@ -49,152 +51,7 @@ *

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 { - /** - * Implementation of {@link EffectiveTextAttributeProvider} that provides effective text - * attributes based on a {@link ReactBaseTextShadowNode} instance and its parent. - */ - private static class HierarchicTextAttributeProvider implements EffectiveTextAttributeProvider { - final private ReactBaseTextShadowNode textShadowNode; - final private TextAttributes parentTextAttributes; - final private TextAttributes textAttributes; - - private HierarchicTextAttributeProvider( - ReactBaseTextShadowNode textShadowNode, - TextAttributes parentTextAttributes, - TextAttributes textAttributes - ) { - this.textShadowNode = textShadowNode; - this.parentTextAttributes = parentTextAttributes; - this.textAttributes = textAttributes; - } - - @Override - @NonNull - public TextTransform getTextTransform() { - return textAttributes.getTextTransform(); - } - - @Nullable - @Override - public Role getRole() { - return textShadowNode.mRole; - } - - @Override - public AccessibilityRole getAccessibilityRole() { - return textShadowNode.mAccessibilityRole; - } - - @Override - public boolean isBackgroundColorSet() { - return textShadowNode.mIsBackgroundColorSet; - } - - @Override - public int getBackgroundColor() { - return textShadowNode.mBackgroundColor; - } - - @Override - public boolean isColorSet() { - return textShadowNode.mIsColorSet; - } - - @Override - public int getColor() { - return textShadowNode.mColor; - } - - @Override - public int getFontStyle() { - return textShadowNode.mFontStyle; - } - - @Override - public int getFontWeight() { - return textShadowNode.mFontWeight; - } - - @Override - public String getFontFamily() { - return textShadowNode.mFontFamily; - } - - @Override - public String getFontFeatureSettings() { - return textShadowNode.mFontFeatureSettings; - } - - @Override - public boolean isUnderlineTextDecorationSet() { - return textShadowNode.mIsUnderlineTextDecorationSet; - } - - @Override - public boolean isLineThroughTextDecorationSet() { - return textShadowNode.mIsLineThroughTextDecorationSet; - } - - @Override - public float getTextShadowOffsetDx() { - return textShadowNode.mTextShadowOffsetDx; - } - - @Override - public float getTextShadowOffsetDy() { - return textShadowNode.mTextShadowOffsetDy; - } - - @Override - public float getTextShadowRadius() { - return textShadowNode.mTextShadowRadius; - } - - @Override - public int getTextShadowColor() { - return textShadowNode.mTextShadowColor; - } - - @Override - public float getEffectiveLetterSpacing() { - final float letterSpacing = textAttributes.getEffectiveLetterSpacing(); - - if (!Float.isNaN(letterSpacing) - && (parentTextAttributes == null - || parentTextAttributes.getEffectiveLetterSpacing() != letterSpacing)) { - return letterSpacing; - } else { - return Float.NaN; - } - } - - @Override - public int getEffectiveFontSize() { - final int fontSize = textAttributes.getEffectiveFontSize(); - - if ( - // `getEffectiveFontSize` always returns a value so don't need to check for anything like `Float.NaN`. - parentTextAttributes == null - || parentTextAttributes.getEffectiveFontSize() != fontSize) { - return fontSize; - } else { - return UNSET; - } - } - - @Override - public float getEffectiveLineHeight() { - final float lineHeight = textAttributes.getEffectiveLineHeight(); - if (!Float.isNaN(lineHeight) - && (parentTextAttributes == null - || parentTextAttributes.getEffectiveLineHeight() != lineHeight)) { - return lineHeight; - } else { - return Float.NaN; - } - } - } +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. @@ -546,6 +403,11 @@ public void setFontSize(float fontSize) { markUpdated(); } + @Override + public int getColor() { + return mColor; + } + @ReactProp(name = ViewProps.COLOR, customType = "Color") public void setColor(@Nullable Integer color) { mIsColorSet = (color != null); @@ -555,6 +417,16 @@ public void setColor(@Nullable Integer color) { markUpdated(); } + @Override + public boolean isColorSet() { + return mIsColorSet; + } + + @Override + public int getBackgroundColor() { + return mBackgroundColor; + } + @ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color") public void setBackgroundColor(@Nullable Integer color) { // Background color needs to be handled here for virtual nodes so it can be incorporated into @@ -570,6 +442,16 @@ public void setBackgroundColor(@Nullable Integer color) { } } + @Override + public boolean isBackgroundColorSet() { + return mIsBackgroundColorSet; + } + + @Override + public @Nullable AccessibilityRole getAccessibilityRole() { + return mAccessibilityRole; + } + @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) public void setAccessibilityRole(@Nullable String accessibilityRole) { if (isVirtual()) { @@ -578,6 +460,11 @@ public void setAccessibilityRole(@Nullable String accessibilityRole) { } } + @Override + public @Nullable Role getRole() { + return mRole; + } + @ReactProp(name = ViewProps.ROLE) public void setRole(@Nullable String role) { if (isVirtual()) { @@ -586,12 +473,22 @@ public void setRole(@Nullable String role) { } } + @Override + public String getFontFamily() { + return mFontFamily; + } + @ReactProp(name = ViewProps.FONT_FAMILY) public void setFontFamily(@Nullable String fontFamily) { mFontFamily = fontFamily; markUpdated(); } + @Override + public int getFontWeight() { + return mFontWeight; + } + @ReactProp(name = ViewProps.FONT_WEIGHT) public void setFontWeight(@Nullable String fontWeightString) { int fontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString); @@ -611,6 +508,16 @@ public void setFontVariant(@Nullable ReadableArray fontVariantArray) { } } + @Override + public String getFontFeatureSettings() { + return mFontFeatureSettings; + } + + @Override + public int getFontStyle() { + return mFontStyle; + } + @ReactProp(name = ViewProps.FONT_STYLE) public void setFontStyle(@Nullable String fontStyleString) { int fontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString); @@ -641,6 +548,16 @@ public void setTextDecorationLine(@Nullable String textDecorationLineString) { markUpdated(); } + @Override + public boolean isUnderlineTextDecorationSet() { + return mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return mIsLineThroughTextDecorationSet; + } + @ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY) public void setTextBreakStrategy(@Nullable String textBreakStrategy) { if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) { @@ -678,6 +595,21 @@ public void setTextShadowOffset(ReadableMap offsetMap) { markUpdated(); } + @Override + public float getTextShadowOffsetDx() { + return mTextShadowOffsetDx; + } + + @Override + public float getTextShadowOffsetDy() { + return mTextShadowOffsetDy; + } + + @Override + public float getTextShadowRadius() { + return mTextShadowRadius; + } + @ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1) public void setTextShadowRadius(float textShadowRadius) { if (textShadowRadius != mTextShadowRadius) { @@ -686,6 +618,11 @@ public void setTextShadowRadius(float textShadowRadius) { } } + @Override + public int getTextShadowColor() { + return mTextShadowColor; + } + @ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color") public void setTextShadowColor(int textShadowColor) { if (textShadowColor != mTextShadowColor) { From bf9b113b37f1e34dfb26c3c3785615ef9bcb6ff7 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 16:54:09 +0200 Subject: [PATCH 06/14] Make the text span order consistent between Paper and Fabric ...hoping that the different order wasn't intentional --- .../java/com/facebook/react/views/text/TextLayoutUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java index dc82987b5b64a4..c45ee4378c47f2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java @@ -46,12 +46,12 @@ public static void buildSpannableFromTextFragmentList(Context context, TextFragm addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); } else if (end >= start) { - addLinkSpanIfApplicable(ops, textAttributes, reactTag, start, end); - addColorSpanIfApplicable(ops, textAttributes, start, end); addBackgroundColorSpanIfApplicable(ops, textAttributes, start, end); + addLinkSpanIfApplicable(ops, textAttributes, reactTag, start, end); + addLetterSpacingSpanIfApplicable(ops, textAttributes, start, end); addFontSizeSpanIfApplicable(ops, textAttributes, start, end); From 9013032ad86e1c2420e93da1e4f6205045176218 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 16:57:40 +0200 Subject: [PATCH 07/14] Move all text attribute spans adds to a single shared method ...which is now possible, because they have the same order --- .../views/text/ReactBaseTextShadowNode.java | 23 +------ .../react/views/text/TextLayoutUtils.java | 69 ++++++++++--------- 2 files changed, 39 insertions(+), 53 deletions(-) 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 7dd15c0c457837..d202764416fd0d 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 @@ -148,27 +148,8 @@ private static void buildSpannedFromShadowNode( if (end >= start) { final int reactTag = textShadowNode.getReactTag(); - TextLayoutUtils.addColorSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addBackgroundColorSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addLinkSpanIfApplicable(ops, textAttributeProvider, reactTag, start, end); - - TextLayoutUtils.addLetterSpacingSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addFontSizeSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addCustomStyleSpanIfApplicable(ops, textAttributeProvider, textShadowNode.getThemedContext(), start, end); - - TextLayoutUtils.addUnderlineSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addStrikethroughSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addShadowStyleSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addLineHeightSpanIfApplicable(ops, textAttributeProvider, start, end); - - TextLayoutUtils.addReactTagSpan(ops, start, end, reactTag); + TextLayoutUtils.addApplicableTextAttributeSpans( + ops, textAttributeProvider, reactTag, textShadowNode.getThemedContext(), start, end); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java index c45ee4378c47f2..aeafd10d3cbb6e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java @@ -46,27 +46,7 @@ public static void buildSpannableFromTextFragmentList(Context context, TextFragm addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); } else if (end >= start) { - addColorSpanIfApplicable(ops, textAttributes, start, end); - - addBackgroundColorSpanIfApplicable(ops, textAttributes, start, end); - - addLinkSpanIfApplicable(ops, textAttributes, reactTag, start, end); - - addLetterSpacingSpanIfApplicable(ops, textAttributes, start, end); - - addFontSizeSpanIfApplicable(ops, textAttributes, start, end); - - addCustomStyleSpanIfApplicable(ops, textAttributes, context, start, end); - - addUnderlineSpanIfApplicable(ops, textAttributes, start, end); - - addStrikethroughSpanIfApplicable(ops, textAttributes, start, end); - - addShadowStyleSpanIfApplicable(ops, textAttributes, start, end); - - addLineHeightSpanIfApplicable(ops, textAttributes, start, end); - - addReactTagSpan(ops, start, end, reactTag); + addApplicableTextAttributeSpans(ops, textAttributes, reactTag, context, start, end); } } } @@ -82,8 +62,33 @@ public static void addInlineViewPlaceholderSpan(List ops, Span new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); } + public static void addApplicableTextAttributeSpans(List ops, + EffectiveTextAttributeProvider textAttributeProvider, int reactTag, Context context, + int start, int end) { + 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); + } - public static void addLinkSpanIfApplicable(List ops, + private static void addLinkSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int reactTag, int start, int end) { boolean roleIsLink = textAttributeProvider.getRole() != null ? @@ -94,7 +99,7 @@ public static void addLinkSpanIfApplicable(List ops, } } - public static void addColorSpanIfApplicable(List ops, + private static void addColorSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { if (textAttributeProvider.isColorSet()) { @@ -102,7 +107,7 @@ public static void addColorSpanIfApplicable(List ops, } } - public static void addBackgroundColorSpanIfApplicable(List ops, + private static void addBackgroundColorSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { if (textAttributeProvider.isBackgroundColorSet()) { @@ -111,7 +116,7 @@ public static void addBackgroundColorSpanIfApplicable(List ops } } - public static void addLetterSpacingSpanIfApplicable(List ops, + private static void addLetterSpacingSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start , int end) { final float effectiveLetterSpacing = textAttributeProvider.getEffectiveLetterSpacing(); @@ -122,7 +127,7 @@ public static void addLetterSpacingSpanIfApplicable(List ops, } - public static void addFontSizeSpanIfApplicable(List ops, + private static void addFontSizeSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { final int effectiveFontSize = textAttributeProvider.getEffectiveFontSize(); @@ -132,7 +137,7 @@ public static void addFontSizeSpanIfApplicable(List ops, } } - public static void addCustomStyleSpanIfApplicable(List ops, + private static void addCustomStyleSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, Context context, int start, int end) { final int fontStyle = textAttributeProvider.getFontStyle(); @@ -146,7 +151,7 @@ public static void addCustomStyleSpanIfApplicable(List ops, } } - public static void addUnderlineSpanIfApplicable(List ops, + private static void addUnderlineSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { if (textAttributeProvider.isUnderlineTextDecorationSet()) { @@ -154,7 +159,7 @@ public static void addUnderlineSpanIfApplicable(List ops, } } - public static void addStrikethroughSpanIfApplicable(List ops, + private static void addStrikethroughSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start , int end) { if (textAttributeProvider.isLineThroughTextDecorationSet()) { @@ -163,7 +168,7 @@ public static void addStrikethroughSpanIfApplicable(List ops, } - public static void addShadowStyleSpanIfApplicable(List ops, + private static void addShadowStyleSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { if ((textAttributeProvider.getTextShadowOffsetDx() != 0 || textAttributeProvider.getTextShadowOffsetDy() != 0 || textAttributeProvider.getTextShadowRadius() != 0) && Color.alpha(textAttributeProvider.getTextShadowColor()) != 0) { @@ -173,7 +178,7 @@ public static void addShadowStyleSpanIfApplicable(List ops, } } - public static void addLineHeightSpanIfApplicable(List ops, + private static void addLineHeightSpanIfApplicable(List ops, EffectiveTextAttributeProvider textAttributeProvider, int start, int end) { final float effectiveLineHeight = textAttributeProvider.getEffectiveLineHeight(); @@ -182,7 +187,7 @@ public static void addLineHeightSpanIfApplicable(List ops, } } - public static void addReactTagSpan(List ops, int start, int end, int reactTag) { + private static void addReactTagSpan(List ops, int start, int end, int reactTag) { ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); } From e026117b5c6c7733186bbbc5863634c66cec2070 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 14 Oct 2023 14:28:05 +0200 Subject: [PATCH 08/14] Port `TextLayoutUtils` to Kotlin --- .../react/views/text/TextLayoutUtils.java | 194 ------------ .../react/views/text/TextLayoutUtils.kt | 287 ++++++++++++++++++ 2 files changed, 287 insertions(+), 194 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java deleted file mode 100644 index aeafd10d3cbb6e..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.java +++ /dev/null @@ -1,194 +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.content.Context; -import android.graphics.Color; -import android.text.*; -import android.view.View; -import com.facebook.react.common.assets.ReactFontManager; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.ReactAccessibilityDelegate; -import com.facebook.react.views.text.fragments.TextFragment; -import com.facebook.react.views.text.fragments.TextFragmentList; - -import java.util.List; - -/** - * Class containing static methods for building {@link Spannable}s - */ -public class TextLayoutUtils { - private static final String INLINE_VIEW_PLACEHOLDER = "0"; - private static final int UNSET = ReactFontManager.TypefaceStyle.UNSET; - - public static void buildSpannableFromTextFragmentList(Context context, TextFragmentList textFragmentList, - SpannableStringBuilder sb, List ops) { - - for (int i = 0, length = textFragmentList.getCount(); i < length; i++) { - final TextFragment fragment = textFragmentList.getFragment(i); - int start = sb.length(); - - // ReactRawText - TextAttributeProps textAttributes = fragment.getTextAttributeProps(); - - TextLayoutUtils.addText(sb, fragment.getString(), textAttributes); - - int end = sb.length(); - int reactTag = fragment.hasReactTag() ? fragment.getReactTag() : View.NO_ID; - if (fragment.hasIsAttachment() && fragment.isAttachment()) { - float width = PixelUtil.toPixelFromSP(fragment.getWidth()); - float height = PixelUtil.toPixelFromSP(fragment.getHeight()); - - addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); - } else if (end >= start) { - addApplicableTextAttributeSpans(ops, textAttributes, reactTag, context, start, end); - } - } - } - - public static void addText(SpannableStringBuilder sb, String text, - EffectiveTextAttributeProvider textAttributeProvider) { - sb.append(TextTransform.apply(text, textAttributeProvider.getTextTransform())); - } - - public static void addInlineViewPlaceholderSpan(List ops, SpannableStringBuilder sb, int reactTag - , float width, float height) { - ops.add(new SetSpanOperation(sb.length() - INLINE_VIEW_PLACEHOLDER.length(), sb.length(), - new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); - } - - public static void addApplicableTextAttributeSpans(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int reactTag, Context context, - int start, int end) { - 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); - } - - private static void addLinkSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int reactTag, - int start, int end) { - boolean roleIsLink = textAttributeProvider.getRole() != null ? - textAttributeProvider.getRole() == ReactAccessibilityDelegate.Role.LINK : - textAttributeProvider.getAccessibilityRole() == ReactAccessibilityDelegate.AccessibilityRole.LINK; - if (roleIsLink) { - ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); - } - } - - private static void addColorSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start, - int end) { - if (textAttributeProvider.isColorSet()) { - ops.add(new SetSpanOperation(start, end, new ReactForegroundColorSpan(textAttributeProvider.getColor()))); - } - } - - private static void addBackgroundColorSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, - int start, int end) { - if (textAttributeProvider.isBackgroundColorSet()) { - ops.add(new SetSpanOperation(start, end, - new ReactBackgroundColorSpan(textAttributeProvider.getBackgroundColor()))); - } - } - - private static void addLetterSpacingSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start - , int end) { - final float effectiveLetterSpacing = textAttributeProvider.getEffectiveLetterSpacing(); - - if (!Float.isNaN(effectiveLetterSpacing)) { - ops.add(new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing))); - } - } - - - private static void addFontSizeSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start, - int end) { - final int effectiveFontSize = textAttributeProvider.getEffectiveFontSize(); - - if (effectiveFontSize != UNSET) { - ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); - } - } - - private static void addCustomStyleSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, - Context context, int start, int end) { - final int fontStyle = textAttributeProvider.getFontStyle(); - final int fontWeight = textAttributeProvider.getFontWeight(); - final String fontFamily = textAttributeProvider.getFontFamily(); - - if (fontStyle != UNSET || fontWeight != UNSET || fontFamily != null) { - ops.add(new SetSpanOperation(start, end, new CustomStyleSpan(fontStyle, - fontWeight, textAttributeProvider.getFontFeatureSettings(), - fontFamily, context.getAssets()))); - } - } - - private static void addUnderlineSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start, - int end) { - if (textAttributeProvider.isUnderlineTextDecorationSet()) { - ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); - } - } - - private static void addStrikethroughSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start - , int end) { - if (textAttributeProvider.isLineThroughTextDecorationSet()) { - ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); - } - } - - - private static void addShadowStyleSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start, - int end) { - if ((textAttributeProvider.getTextShadowOffsetDx() != 0 || textAttributeProvider.getTextShadowOffsetDy() != 0 || textAttributeProvider.getTextShadowRadius() != 0) && Color.alpha(textAttributeProvider.getTextShadowColor()) != 0) { - ops.add(new SetSpanOperation(start, end, new ShadowStyleSpan(textAttributeProvider.getTextShadowOffsetDx(), - textAttributeProvider.getTextShadowOffsetDy(), textAttributeProvider.getTextShadowRadius(), - textAttributeProvider.getTextShadowColor()))); - } - } - - private static void addLineHeightSpanIfApplicable(List ops, - EffectiveTextAttributeProvider textAttributeProvider, int start, - int end) { - final float effectiveLineHeight = textAttributeProvider.getEffectiveLineHeight(); - if (!Float.isNaN(effectiveLineHeight)) { - ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight))); - } - } - - private static void addReactTagSpan(List ops, int start, int end, int reactTag) { - ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); - - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt new file mode 100644 index 00000000000000..bedbeb52fe64c0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt @@ -0,0 +1,287 @@ +/* + * 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.assets.ReactFontManager +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" + private const val UNSET = ReactFontManager.TypefaceStyle.UNSET + + @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.getTextAttributeProps() + + addText(sb, fragment.getString(), textAttributes) + + val end = sb.length + val reactTag = if (fragment.hasReactTag()) fragment.getReactTag() else View.NO_ID + if (fragment.hasIsAttachment() && fragment.isAttachment()) { + val width = PixelUtil.toPixelFromSP(fragment.getWidth()) + val height = PixelUtil.toPixelFromSP(fragment.getHeight()) + + 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 != 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 != UNSET || fontWeight != 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))) + } +} From ce9fa4a0d975fb955038dc429227e7028d9b7a2b Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 23 Sep 2023 17:08:07 +0200 Subject: [PATCH 09/14] Extract some helper methods in `ReactBaseTextShadowNode` ...for consistency with the de-duplicated code --- .../views/text/ReactBaseTextShadowNode.java | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) 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 d202764416fd0d..c9643d5fe046f5 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 @@ -104,40 +104,11 @@ private static void buildSpannedFromShadowNode( 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())); + addInlineImageSpan(ops, sb, (ReactTextInlineImageShadowNode) child); } 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); + addInlineViewPlaceholderSpan(ops, sb, child); - TextLayoutUtils.addInlineViewPlaceholderSpan(ops, sb, reactTag, width, height); - - inlineViews.put(reactTag, child); + inlineViews.put(child.getReactTag(), child); } else { throw new IllegalViewOperationException( "Unexpected view type nested under a or node: " + child.getClass()); @@ -153,6 +124,40 @@ private static void buildSpannedFromShadowNode( } } + 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, From 4f4cd8b063a9ffb8d66e5a72ed3250c384cf4f46 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Sat, 16 Dec 2023 15:25:25 +0100 Subject: [PATCH 10/14] Add a feature flag for the unified `Spannable` building logic ...temporarily bringing back the old logic. --- .../react/config/ReactFeatureFlags.java | 3 + .../views/text/ReactBaseTextShadowNode.java | 193 +++++++++++++++++- .../react/views/text/TextAttributeProps.java | 8 +- .../react/views/text/TextLayoutManager.java | 116 +++++++++++ .../text/TextLayoutManagerMapBuffer.java | 112 +++++++++- 5 files changed, 424 insertions(+), 8 deletions(-) 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 ea1b2ba963749b..afc812cbac2f60 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 @@ -185,4 +185,7 @@ public class ReactFeatureFlags { /** When enabled, the default value of the position style property is relative. */ public static boolean positionRelativeDefault = 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/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index c9643d5fe046f5..ea83c3406f679d 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 @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import android.graphics.Color; import android.graphics.Typeface; import android.os.Build; import android.text.Layout; @@ -21,9 +22,10 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.assets.ReactFontManager; -import com.facebook.react.internal.views.text.BasicTextAttributeProvider; -import com.facebook.react.internal.views.text.HierarchicTextAttributeProvider; -import com.facebook.react.internal.views.text.TextLayoutUtils; +import com.facebook.react.views.text.BasicTextAttributeProvider; +import com.facebook.react.views.text.HierarchicTextAttributeProvider; +import com.facebook.react.views.text.TextLayoutUtils; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; @@ -72,6 +74,189 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode implement protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback; private static void buildSpannedFromShadowNode( + ReactBaseTextShadowNode textShadowNode, + SpannableStringBuilder sb, + List ops, + @Nullable TextAttributes parentTextAttributes, + boolean supportsInlineViews, + @Nullable Map inlineViews, + int start) { + if (ReactFeatureFlags.enableSpannableBuildingUnification) { + buildSpannedFromShadowNodeUnified( + textShadowNode, + sb, + ops, + parentTextAttributes, + supportsInlineViews, + inlineViews, + start + ); + } else { + buildSpannedFromShadowNodeDuplicated( + textShadowNode, + sb, + ops, + parentTextAttributes, + supportsInlineViews, + inlineViews, + start + ); + } + } + + private static void buildSpannedFromShadowNodeDuplicated( + ReactBaseTextShadowNode textShadowNode, + SpannableStringBuilder sb, + List ops, + @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) { + sb.append( + TextTransform.apply( + ((ReactRawTextShadowNode) child).getText(), textAttributes.getTextTransform())); + } else if (child instanceof ReactBaseTextShadowNode) { + buildSpannedFromShadowNodeDuplicated( + (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))); + 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 != UNSET + || textShadowNode.mFontWeight != 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()))); + } + } + + private static void buildSpannedFromShadowNodeUnified( ReactBaseTextShadowNode textShadowNode, SpannableStringBuilder sb, List ops, @@ -95,7 +280,7 @@ private static void buildSpannedFromShadowNode( if (child instanceof ReactRawTextShadowNode) { TextLayoutUtils.addText(sb, ((ReactRawTextShadowNode) child).getText(), textAttributeProvider); } else if (child instanceof ReactBaseTextShadowNode) { - buildSpannedFromShadowNode( + buildSpannedFromShadowNodeUnified( (ReactBaseTextShadowNode) child, sb, ops, 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 1f4f9bfa88374d..564317f1a2346b 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 @@ -380,8 +380,7 @@ public TextTransform getTextTransform() { return mTextTransform; } - @Override - public float getEffectiveLetterSpacing() { + public float getLetterSpacing() { float letterSpacingPixels = mAllowFontScaling ? PixelUtil.toPixelFromSP(mLetterSpacingInput) @@ -396,6 +395,11 @@ public float getEffectiveLetterSpacing() { return letterSpacingPixels / mFontSize; } + @Override + public float getEffectiveLetterSpacing() { + return getLetterSpacing(); + } + @Override public int getEffectiveFontSize() { return mFontSize; 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 61213a6a283a1c..709d285f8c7b8f 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 @@ -10,6 +10,7 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; +import android.graphics.Color; import android.os.Build; import android.text.BoringLayout; import android.text.Layout; @@ -20,6 +21,7 @@ import android.text.TextPaint; import android.util.LayoutDirection; import android.util.LruCache; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -30,7 +32,11 @@ import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableArray; 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; @@ -100,6 +106,116 @@ private static void buildSpannableFromFragments( 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, + List ops) { + + for (int i = 0, length = fragments.size(); i < length; i++) { + ReadableMap fragment = fragments.getMap(i); + int start = sb.length(); + + // ReactRawText + TextAttributeProps textAttributes = + TextAttributeProps.fromReadableMap( + new ReactStylesDiffMap(fragment.getMap("textAttributes"))); + + sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform)); + + int end = sb.length(); + int reactTag = fragment.hasKey("reactTag") ? fragment.getInt("reactTag") : View.NO_ID; + if (fragment.hasKey(ViewProps.IS_ATTACHMENT) + && fragment.getBoolean(ViewProps.IS_ATTACHMENT)) { + float width = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.WIDTH)); + float height = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.HEIGHT)); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + } else if (end >= start) { + boolean roleIsLink = + textAttributes.mRole != null + ? textAttributes.mRole == Role.LINK + : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { + ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); + } + if (textAttributes.mIsColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactForegroundColorSpan(textAttributes.mColor))); + } + if (textAttributes.mIsBackgroundColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); + } + if (!Float.isNaN(textAttributes.getLetterSpacing())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); + } + ops.add( + new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); + if (textAttributes.mFontStyle != UNSET + || textAttributes.mFontWeight != UNSET + || textAttributes.mFontFamily != null) { + ops.add( + new SetSpanOperation( + start, + end, + new CustomStyleSpan( + textAttributes.mFontStyle, + textAttributes.mFontWeight, + textAttributes.mFontFeatureSettings, + textAttributes.mFontFamily, + context.getAssets()))); + } + if (textAttributes.mIsUnderlineTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); + } + if (textAttributes.mIsLineThroughTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); + } + if ((textAttributes.mTextShadowOffsetDx != 0 + || textAttributes.mTextShadowOffsetDy != 0 + || textAttributes.mTextShadowRadius != 0) + && Color.alpha(textAttributes.mTextShadowColor) != 0) { + ops.add( + new SetSpanOperation( + start, + end, + new ShadowStyleSpan( + textAttributes.mTextShadowOffsetDx, + textAttributes.mTextShadowOffsetDy, + textAttributes.mTextShadowRadius, + textAttributes.mTextShadowColor))); + } + if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); + } + + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + } + } + } + + private static void buildSpannableFromFragmentsUnified( + Context context, + ReadableArray fragments, + SpannableStringBuilder sb, + List ops) { final var textFragmentList = new BridgeTextFragmentList(fragments); 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 e1e34c9baf7d32..e99cd786f73878 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 @@ -11,6 +11,7 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; +import android.graphics.Color; import android.os.Build; import android.text.BoringLayout; import android.text.Layout; @@ -21,6 +22,7 @@ import android.text.TextPaint; import android.util.LayoutDirection; import android.util.LruCache; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -30,7 +32,10 @@ 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; @@ -120,8 +125,111 @@ public static boolean isRTL(MapBuffer attributedString) { == LayoutDirection.RTL; } - private static void buildSpannableFromFragments(Context context, MapBuffer fragments, SpannableStringBuilder sb, - List ops) { + 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++) { + MapBuffer fragment = fragments.getMapBuffer(i); + int start = sb.length(); + + TextAttributeProps textAttributes = + TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES)); + + sb.append( + TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform)); + + int end = sb.length(); + int reactTag = + fragment.contains(FR_KEY_REACT_TAG) ? fragment.getInt(FR_KEY_REACT_TAG) : View.NO_ID; + if (fragment.contains(FR_KEY_IS_ATTACHMENT) && fragment.getBoolean(FR_KEY_IS_ATTACHMENT)) { + float width = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_WIDTH)); + float height = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_HEIGHT)); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + } else if (end >= start) { + boolean roleIsLink = + textAttributes.mRole != null + ? textAttributes.mRole == Role.LINK + : textAttributes.mAccessibilityRole == AccessibilityRole.LINK; + if (roleIsLink) { + ops.add(new SetSpanOperation(start, end, new ReactClickableSpan(reactTag))); + } + if (textAttributes.mIsColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactForegroundColorSpan(textAttributes.mColor))); + } + if (textAttributes.mIsBackgroundColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); + } + if (!Float.isNaN(textAttributes.getLetterSpacing())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); + } + ops.add( + new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); + if (textAttributes.mFontStyle != UNSET + || textAttributes.mFontWeight != UNSET + || textAttributes.mFontFamily != null) { + ops.add( + new SetSpanOperation( + start, + end, + new CustomStyleSpan( + textAttributes.mFontStyle, + textAttributes.mFontWeight, + textAttributes.mFontFeatureSettings, + textAttributes.mFontFamily, + context.getAssets()))); + } + if (textAttributes.mIsUnderlineTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); + } + if (textAttributes.mIsLineThroughTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); + } + if ((textAttributes.mTextShadowOffsetDx != 0 + || textAttributes.mTextShadowOffsetDy != 0 + || textAttributes.mTextShadowRadius != 0) + && Color.alpha(textAttributes.mTextShadowColor) != 0) { + ops.add( + new SetSpanOperation( + start, + end, + new ShadowStyleSpan( + textAttributes.mTextShadowOffsetDx, + textAttributes.mTextShadowOffsetDy, + textAttributes.mTextShadowRadius, + textAttributes.mTextShadowColor))); + } + if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); + } + + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + } + } + } + + private static void buildSpannableFromFragmentsUnified( + Context context, MapBuffer fragments, SpannableStringBuilder sb, List ops) { final var textFragmentList = new MapBufferTextFragmentList(fragments); From 2c36651f69075bd61f9e27bee2aeba6c82dd8b90 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Thu, 26 Oct 2023 18:39:20 +0200 Subject: [PATCH 11/14] `TextAttributeProps`: Put the getters in the conventional places --- .../react/views/text/TextAttributeProps.java | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) 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 564317f1a2346b..45cead05e0df1e 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 @@ -425,6 +425,11 @@ private void setFontSize(float fontSize) { mFontSize = (int) fontSize; } + @Override + public int getColor() { + return mColor; + } + private void setColor(@Nullable Integer color) { mIsColorSet = (color != null); if (mIsColorSet) { @@ -432,6 +437,16 @@ private void setColor(@Nullable Integer color) { } } + @Override + public boolean isColorSet() { + return mIsColorSet; + } + + @Override + public int getBackgroundColor() { + return mBackgroundColor; + } + private void setBackgroundColor(Integer color) { // TODO: Don't apply background color to anchor TextView since it will be applied on the View // directly @@ -443,6 +458,21 @@ private void setBackgroundColor(Integer color) { // } } + @Override + public boolean isBackgroundColorSet() { + return mIsBackgroundColorSet; + } + + @Override + public int getFontStyle() { + return mFontStyle; + } + + @Override + public String getFontFamily() { + return mFontFamily; + } + private void setFontFamily(@Nullable String fontFamily) { mFontFamily = fontFamily; } @@ -545,6 +575,16 @@ private void setFontVariant(@Nullable MapBuffer fontVariant) { mFontFeatureSettings = TextUtils.join(", ", features); } + @Override + public String getFontFeatureSettings() { + return mFontFeatureSettings; + } + + @Override + public int getFontWeight() { + return mFontWeight; + } + private void setFontWeight(@Nullable String fontWeightString) { mFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString); } @@ -571,6 +611,16 @@ private void setTextDecorationLine(@Nullable String textDecorationLineString) { } } + @Override + public boolean isUnderlineTextDecorationSet() { + return mIsUnderlineTextDecorationSet; + } + + @Override + public boolean isLineThroughTextDecorationSet() { + return mIsLineThroughTextDecorationSet; + } + private void setTextShadowOffset(ReadableMap offsetMap) { mTextShadowOffsetDx = 0; mTextShadowOffsetDy = 0; @@ -589,10 +639,20 @@ private void setTextShadowOffset(ReadableMap offsetMap) { } } + @Override + public float getTextShadowOffsetDx() { + return mTextShadowOffsetDx; + } + private void setTextShadowOffsetDx(float dx) { mTextShadowOffsetDx = PixelUtil.toPixelFromDIP(dx); } + @Override + public float getTextShadowOffsetDy() { + return mTextShadowOffsetDy; + } + private void setTextShadowOffsetDy(float dy) { mTextShadowOffsetDy = PixelUtil.toPixelFromDIP(dy); } @@ -616,12 +676,22 @@ private void setLayoutDirection(@Nullable String layoutDirection) { mLayoutDirection = getLayoutDirection(layoutDirection); } + @Override + public float getTextShadowRadius() { + return mTextShadowRadius; + } + private void setTextShadowRadius(float textShadowRadius) { if (textShadowRadius != mTextShadowRadius) { mTextShadowRadius = textShadowRadius; } } + @Override + public int getTextShadowColor() { + return mTextShadowColor; + } + private void setTextShadowColor(int textShadowColor) { if (textShadowColor != mTextShadowColor) { mTextShadowColor = textShadowColor; @@ -643,6 +713,11 @@ private void setTextTransform(@Nullable String textTransform) { } } + @Override + public AccessibilityRole getAccessibilityRole() { + return mAccessibilityRole; + } + private void setAccessibilityRole(@Nullable String accessibilityRole) { if (accessibilityRole == null) { mAccessibilityRole = null; @@ -651,6 +726,12 @@ private void setAccessibilityRole(@Nullable String accessibilityRole) { } } + @Nullable + @Override + public Role getRole() { + return mRole; + } + private void setRole(@Nullable String role) { if (role == null) { mRole = null; @@ -698,85 +779,4 @@ public static int getHyphenationFrequency(@Nullable String hyphenationFrequency) } return androidHyphenationFrequency; } - - @Nullable - @Override - public Role getRole() { - return mRole; - } - - @Override - public AccessibilityRole getAccessibilityRole() { - return mAccessibilityRole; - } - - @Override - public boolean isBackgroundColorSet() { - return mIsBackgroundColorSet; - } - - @Override - public int getBackgroundColor() { - return mBackgroundColor; - } - - @Override - public boolean isColorSet() { - return mIsColorSet; - } - - @Override - public int getColor() { - return mColor; - } - - @Override - public int getFontStyle() { - return mFontStyle; - } - - @Override - public int getFontWeight() { - return mFontWeight; - } - - @Override - public String getFontFamily() { - return mFontFamily; - } - - @Override - public String getFontFeatureSettings() { - return mFontFeatureSettings; - } - - @Override - public boolean isUnderlineTextDecorationSet() { - return mIsUnderlineTextDecorationSet; - } - - @Override - public boolean isLineThroughTextDecorationSet() { - return mIsLineThroughTextDecorationSet; - } - - @Override - public float getTextShadowOffsetDx() { - return mTextShadowOffsetDx; - } - - @Override - public float getTextShadowOffsetDy() { - return mTextShadowOffsetDy; - } - - @Override - public float getTextShadowRadius() { - return mTextShadowRadius; - } - - @Override - public int getTextShadowColor() { - return mTextShadowColor; - } } From ce3df0ec9c79ac1f02e2b9bc3560f1b28cf2c5dd Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Wed, 3 Jan 2024 14:26:35 +0100 Subject: [PATCH 12/14] `text/fragments`: Drop the `mFoo` naming convention --- .../views/text/fragments/BridgeTextFragment.kt | 18 +++++++++--------- .../text/fragments/BridgeTextFragmentList.kt | 7 +++---- .../fragments/MapBufferTextFragmentList.kt | 6 +++--- 3 files changed, 15 insertions(+), 16 deletions(-) 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 index 39a7587997ccd8..08502ebd87cde4 100644 --- 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 @@ -8,21 +8,21 @@ import com.facebook.react.views.text.TextAttributeProps /** * A [TextFragment] implementation backed by a a [ReadableMap] */ -internal class BridgeTextFragment(private val mFragment: ReadableMap) : TextFragment { +internal class BridgeTextFragment(private val fragment: ReadableMap) : TextFragment { override fun getTextAttributeProps(): TextAttributeProps = - TextAttributeProps.fromReadableMap(ReactStylesDiffMap(mFragment.getMap("textAttributes"))) + TextAttributeProps.fromReadableMap(ReactStylesDiffMap(fragment.getMap("textAttributes"))) - override fun getString(): String? = mFragment.getString("string") + override fun getString(): String? = fragment.getString("string") - override fun hasReactTag(): Boolean = mFragment.hasKey("reactTag") + override fun hasReactTag(): Boolean = fragment.hasKey("reactTag") - override fun getReactTag(): Int = mFragment.getInt("reactTag") + override fun getReactTag(): Int = fragment.getInt("reactTag") - override fun hasIsAttachment(): Boolean = mFragment.hasKey(ViewProps.IS_ATTACHMENT) + override fun hasIsAttachment(): Boolean = fragment.hasKey(ViewProps.IS_ATTACHMENT) - override fun isAttachment(): Boolean = mFragment.getBoolean(ViewProps.IS_ATTACHMENT) + override fun isAttachment(): Boolean = fragment.getBoolean(ViewProps.IS_ATTACHMENT) - override fun getWidth(): Double = mFragment.getDouble(ViewProps.WIDTH) + override fun getWidth(): Double = fragment.getDouble(ViewProps.WIDTH) - override fun getHeight(): Double = mFragment.getDouble(ViewProps.HEIGHT) + override fun getHeight(): Double = 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 index 8c1bab6c389108..b57622fc72a6f4 100644 --- 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 @@ -5,10 +5,9 @@ import com.facebook.react.bridge.ReadableArray /** * A list of [TextFragment]s backed by a [ReadableArray] */ -internal class BridgeTextFragmentList(private val mFragments: ReadableArray) : TextFragmentList { - - override fun getFragment(index: Int): TextFragment = BridgeTextFragment(mFragments.getMap(index)) +internal class BridgeTextFragmentList(private val fragments: ReadableArray) : TextFragmentList { + override fun getFragment(index: Int): TextFragment = BridgeTextFragment(fragments.getMap(index)) override val count: Int - get() = mFragments.size() + get() = fragments.size() } 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 index 82365340442885..75f76cefc41a78 100644 --- 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 @@ -5,10 +5,10 @@ import com.facebook.react.common.mapbuffer.MapBuffer /** * A list of [TextFragment]s backed by a [MapBuffer] */ -internal class MapBufferTextFragmentList(private val mFragments: MapBuffer) : TextFragmentList { +internal class MapBufferTextFragmentList(private val fragments: MapBuffer) : TextFragmentList { override fun getFragment(index: Int): TextFragment = - MapBufferTextFragment(mFragments.getMapBuffer(index)) + MapBufferTextFragment(fragments.getMapBuffer(index)) override val count: Int - get() = mFragments.count + get() = fragments.count } From 8256a1b8c86de45c3f251407a817732ad739afd7 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Wed, 3 Jan 2024 14:27:22 +0100 Subject: [PATCH 13/14] `TextFragment`: Switch to Kotlin-style properties --- .../react/views/text/TextLayoutUtils.kt | 12 ++++++------ .../text/fragments/BridgeTextFragment.kt | 19 ++++++++++++------- .../text/fragments/MapBufferTextFragment.kt | 19 ++++++++++++------- .../views/text/fragments/TextFragment.kt | 12 ++++++------ 4 files changed, 36 insertions(+), 26 deletions(-) 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 index bedbeb52fe64c0..3fa90ad741f671 100644 --- 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 @@ -36,15 +36,15 @@ internal object TextLayoutUtils { val start = sb.length // ReactRawText - val textAttributes = fragment.getTextAttributeProps() + val textAttributes = fragment.textAttributeProps - addText(sb, fragment.getString(), textAttributes) + addText(sb, fragment.string, textAttributes) val end = sb.length - val reactTag = if (fragment.hasReactTag()) fragment.getReactTag() else View.NO_ID - if (fragment.hasIsAttachment() && fragment.isAttachment()) { - val width = PixelUtil.toPixelFromSP(fragment.getWidth()) - val height = PixelUtil.toPixelFromSP(fragment.getHeight()) + 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) { 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 index 08502ebd87cde4..fd09104d8f1f0c 100644 --- 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 @@ -9,20 +9,25 @@ import com.facebook.react.views.text.TextAttributeProps * A [TextFragment] implementation backed by a a [ReadableMap] */ internal class BridgeTextFragment(private val fragment: ReadableMap) : TextFragment { - override fun getTextAttributeProps(): TextAttributeProps = - TextAttributeProps.fromReadableMap(ReactStylesDiffMap(fragment.getMap("textAttributes"))) + override val textAttributeProps: TextAttributeProps + get() = TextAttributeProps.fromReadableMap(ReactStylesDiffMap(fragment.getMap("textAttributes"))) - override fun getString(): String? = fragment.getString("string") + override val string: String? + get() = fragment.getString("string") override fun hasReactTag(): Boolean = fragment.hasKey("reactTag") - override fun getReactTag(): Int = fragment.getInt("reactTag") + override val reactTag: Int + get() = fragment.getInt("reactTag") override fun hasIsAttachment(): Boolean = fragment.hasKey(ViewProps.IS_ATTACHMENT) - override fun isAttachment(): Boolean = fragment.getBoolean(ViewProps.IS_ATTACHMENT) + override val isAttachment: Boolean + get() = fragment.getBoolean(ViewProps.IS_ATTACHMENT) - override fun getWidth(): Double = fragment.getDouble(ViewProps.WIDTH) + override val width: Double + get() = fragment.getDouble(ViewProps.WIDTH) - override fun getHeight(): Double = fragment.getDouble(ViewProps.HEIGHT) + 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/MapBufferTextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/MapBufferTextFragment.kt index 2709e89b896bf7..6e11e7a996fb4e 100644 --- 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 @@ -14,20 +14,25 @@ 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 fun getTextAttributeProps(): TextAttributeProps = - TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES.toInt())) + override val textAttributeProps: TextAttributeProps + get() = TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES.toInt())) - override fun getString(): String = fragment.getString(FR_KEY_STRING.toInt()) + override val string: String + get() = fragment.getString(FR_KEY_STRING.toInt()) override fun hasReactTag(): Boolean = fragment.contains(FR_KEY_REACT_TAG.toInt()) - override fun getReactTag(): Int = fragment.getInt(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 fun isAttachment(): Boolean = fragment.getBoolean(FR_KEY_IS_ATTACHMENT.toInt()) + override val isAttachment: Boolean + get() = fragment.getBoolean(FR_KEY_IS_ATTACHMENT.toInt()) - override fun getWidth(): Double = fragment.getDouble(FR_KEY_WIDTH.toInt()) + override val width: Double + get() = fragment.getDouble(FR_KEY_WIDTH.toInt()) - override fun getHeight(): Double = fragment.getDouble(FR_KEY_HEIGHT.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/TextFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/fragments/TextFragment.kt index 68e95e2d2ac014..7d86c345fe06f3 100644 --- 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 @@ -6,19 +6,19 @@ import com.facebook.react.views.text.TextAttributeProps * Interface for a text fragment */ internal interface TextFragment { - fun getTextAttributeProps(): TextAttributeProps + val textAttributeProps: TextAttributeProps - fun getString(): String? + val string: String? fun hasReactTag(): Boolean - fun getReactTag(): Int + val reactTag: Int fun hasIsAttachment(): Boolean - fun isAttachment(): Boolean + val isAttachment: Boolean - fun getWidth(): Double + val width: Double - fun getHeight(): Double + val height: Double } From 254c5c2a6971c3aab74cbd9bd0dac017581eb9c2 Mon Sep 17 00:00:00 2001 From: Jakub Trzebiatowski Date: Wed, 3 Jan 2024 14:59:25 +0100 Subject: [PATCH 14/14] Extract the `UNSET = -1` constant --- .../com/facebook/react/common/IntConstants.kt | 14 ++++++++ .../react/common/assets/ReactFontManager.java | 12 +++---- .../react/views/text/CustomStyleSpan.java | 5 +-- .../text/EffectiveTextAttributeProvider.kt | 6 +--- .../text/HierarchicTextAttributeProvider.kt | 4 ++- .../views/text/ReactBaseTextShadowNode.java | 20 +++++------ .../react/views/text/ReactTextShadowNode.java | 7 ++-- .../react/views/text/ReactTextUpdate.java | 12 +++---- .../react/views/text/ReactTextView.java | 11 +++--- .../react/views/text/ReactTypefaceUtils.java | 5 +-- .../react/views/text/TextAttributeProps.java | 35 +++++++++---------- .../react/views/text/TextLayoutManager.java | 11 +++--- .../text/TextLayoutManagerMapBuffer.java | 10 +++--- .../react/views/text/TextLayoutUtils.kt | 7 ++-- .../react/views/textinput/ReactEditText.java | 17 +++++---- .../textinput/ReactTextInputShadowNode.java | 7 ++-- 16 files changed, 95 insertions(+), 88 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt new file mode 100644 index 00000000000000..e25f3d0564737f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/IntConstants.kt @@ -0,0 +1,14 @@ +package com.facebook.react.common + +/** + * General-purpose integer constants + */ +internal object IntConstants { + /** + * Some types have built-in support for representing a "missing" or "unset" value, for example + * NaN in the case of floating point numbers or null in the case of object references. Integers + * don't have such a special value. When an integer represent an inherently non-negative value, + * we use a special negative value to mark it as "unset". + */ + const val UNSET: Int = -1 +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java index 7e7df01bd140ff..f2fc361501a7ff 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java @@ -15,6 +15,8 @@ import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.common.IntConstants; + import java.util.HashMap; import java.util.Map; @@ -167,8 +169,6 @@ public static class TypefaceStyle { public static final int BOLD = 700; public static final int NORMAL = 400; - public static final int UNSET = -1; - private static final int MIN_WEIGHT = 1; private static final int MAX_WEIGHT = 1000; @@ -177,11 +177,11 @@ public static class TypefaceStyle { public TypefaceStyle(int weight, boolean italic) { mItalic = italic; - mWeight = weight == UNSET ? NORMAL : weight; + mWeight = weight == IntConstants.UNSET ? NORMAL : weight; } public TypefaceStyle(int style) { - if (style == UNSET) { + if (style == IntConstants.UNSET) { style = Typeface.NORMAL; } @@ -194,12 +194,12 @@ public TypefaceStyle(int style) { * existing weight bit in `style` will be used. */ public TypefaceStyle(int style, int weight) { - if (style == UNSET) { + if (style == IntConstants.UNSET) { style = Typeface.NORMAL; } mItalic = (style & Typeface.ITALIC) != 0; - mWeight = weight == UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight; + mWeight = weight == IntConstants.UNSET ? (style & Typeface.BOLD) != 0 ? BOLD : NORMAL : weight; } public int getNearestStyle() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java index 51fd4dd0149c77..6704f2684d9567 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java @@ -14,6 +14,7 @@ import android.text.style.MetricAffectingSpan; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.assets.ReactFontManager; @Nullsafe(Nullsafe.Mode.LOCAL) @@ -61,11 +62,11 @@ public void updateMeasureState(TextPaint paint) { } public int getStyle() { - return mStyle == ReactFontManager.TypefaceStyle.UNSET ? Typeface.NORMAL : mStyle; + return mStyle == IntConstants.UNSET ? Typeface.NORMAL : mStyle; } public int getWeight() { - return mWeight == ReactFontManager.TypefaceStyle.UNSET + return mWeight == IntConstants.UNSET ? ReactFontManager.TypefaceStyle.NORMAL : mWeight; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EffectiveTextAttributeProvider.kt index 6da9be9759663f..a9179ce7c0cf7c 100644 --- 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 @@ -1,15 +1,11 @@ package com.facebook.react.views.text -import com.facebook.react.common.assets.ReactFontManager +import com.facebook.react.common.IntConstants.UNSET /** * Interface for an entity providing effective text attributes of a text node/fragment */ internal interface EffectiveTextAttributeProvider : BasicTextAttributeProvider { - companion object { - const val UNSET = ReactFontManager.TypefaceStyle.UNSET - } - val textTransform: TextTransform val effectiveLetterSpacing: 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 index 505d6f51eb0f0a..da8568e06330f3 100644 --- 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 @@ -1,5 +1,7 @@ package com.facebook.react.views.text +import com.facebook.react.common.IntConstants + /** * Implementation of [EffectiveTextAttributeProvider] that provides effective text * attributes based on a [ReactBaseTextShadowNode] instance and its parent. @@ -33,7 +35,7 @@ internal class HierarchicTextAttributeProvider( return if (parentTextAttributes == null || parentTextAttributes.effectiveFontSize != fontSize) { fontSize } else { - EffectiveTextAttributeProvider.UNSET + IntConstants.UNSET } } 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 ea83c3406f679d..e6776f2de7b769 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -20,11 +20,8 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.assets.ReactFontManager; -import com.facebook.react.views.text.BasicTextAttributeProvider; -import com.facebook.react.views.text.HierarchicTextAttributeProvider; -import com.facebook.react.views.text.TextLayoutUtils; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; @@ -59,7 +56,6 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode implement // 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"; @@ -212,8 +208,8 @@ private static void buildSpannedFromShadowNodeDuplicated( || parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) { ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize))); } - if (textShadowNode.mFontStyle != UNSET - || textShadowNode.mFontWeight != UNSET + if (textShadowNode.mFontStyle != IntConstants.UNSET + || textShadowNode.mFontWeight != IntConstants.UNSET || textShadowNode.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -429,7 +425,7 @@ protected Spannable spannedFromShadowNode( protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; - protected int mNumberOfLines = UNSET; + protected int mNumberOfLines = IntConstants.UNSET; protected int mTextAlign = Gravity.NO_GRAVITY; protected int mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; protected int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; @@ -451,9 +447,9 @@ protected Spannable spannedFromShadowNode( * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link * Typeface#NORMAL} or {@link Typeface#BOLD}. */ - protected int mFontStyle = UNSET; + protected int mFontStyle = IntConstants.UNSET; - protected int mFontWeight = UNSET; + protected int mFontWeight = IntConstants.UNSET; /** * NB: If a font family is used that does not have a style in a certain Android version (ie. * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text @@ -506,9 +502,9 @@ private int getTextAlign() { return textAlign; } - @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = UNSET) + @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = IntConstants.UNSET) public void setNumberOfLines(int numberOfLines) { - mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; + mNumberOfLines = numberOfLines == 0 ? IntConstants.UNSET : numberOfLines; markUpdated(); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index d53f18a5aa3289..0e7b5fd27fa0fa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.IntConstants; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactShadowNode; @@ -80,7 +81,7 @@ public long measure( int minimumFontSize = (int) Math.max(mMinimumFontScale * initialFontSize, PixelUtil.toPixelFromDIP(4)); while (currentFontSize > minimumFontSize - && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines + && (mNumberOfLines != IntConstants.UNSET && layout.getLineCount() > mNumberOfLines || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) { // TODO: We could probably use a smarter algorithm here. This will require 0(n) // measurements @@ -122,7 +123,7 @@ public long measure( } final int lineCount = - mNumberOfLines == UNSET + mNumberOfLines == IntConstants.UNSET ? layout.getLineCount() : Math.min(mNumberOfLines, layout.getLineCount()); @@ -327,7 +328,7 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( mPreparedSpannableText, - UNSET, + IntConstants.UNSET, mContainsImages, getPadding(Spacing.START), getPadding(Spacing.TOP), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 0802ec3622a1bd..1bfab32967cb93 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -7,11 +7,11 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.text.Layout; import android.text.Spannable; +import com.facebook.react.common.IntConstants; + /** * Class that contains the data needed for a text update. Used by both and * VisibleForTesting from {@link TextInputEventsTestCase}. @@ -67,10 +67,10 @@ public ReactTextUpdate( text, jsEventCounter, containsImages, - UNSET, - UNSET, - UNSET, - UNSET, + IntConstants.UNSET, + IntConstants.UNSET, + IntConstants.UNSET, + IntConstants.UNSET, textAlign, textBreakStrategy, justificationMode); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 83bacaafb9ea60..446d70a87f1726 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -7,8 +7,6 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; @@ -35,6 +33,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactCompoundView; @@ -378,10 +377,10 @@ public void setText(ReactTextUpdate update) { // In Fabric padding is set by the update of Layout Metrics and not as part of the "setText" // operation // TODO T56559197: remove this condition when we migrate 100% to Fabric - if (paddingLeft != UNSET - && paddingTop != UNSET - && paddingRight != UNSET - && paddingBottom != UNSET) { + if (paddingLeft != IntConstants.UNSET + && paddingTop != IntConstants.UNSET + && paddingRight != IntConstants.UNSET + && paddingBottom != IntConstants.UNSET) { setPadding( (int) Math.floor(paddingLeft), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java index 64fe5e36c0eb0d..da103426a9f5f4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.java @@ -13,6 +13,7 @@ import androidx.annotation.Nullable; import com.facebook.infer.annotation.Nullsafe; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.assets.ReactFontManager; import java.util.ArrayList; import java.util.List; @@ -45,7 +46,7 @@ public static int parseFontWeight(@Nullable String fontWeightString) { return 900; } } - return ReactFontManager.TypefaceStyle.UNSET; + return IntConstants.UNSET; } public static int parseFontStyle(@Nullable String fontStyleString) { @@ -57,7 +58,7 @@ public static int parseFontStyle(@Nullable String fontStyleString) { return Typeface.NORMAL; } } - return ReactFontManager.TypefaceStyle.UNSET; + return IntConstants.UNSET; } public static @Nullable String parseFontVariant(@Nullable ReadableArray fontVariantArray) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 45cead05e0df1e..df384e7d616fc6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -17,6 +17,7 @@ import com.facebook.common.logging.FLog; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.uimanager.PixelUtil; @@ -62,8 +63,6 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { 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"; @@ -85,15 +84,15 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { protected boolean mIsBackgroundColorSet = false; protected int mBackgroundColor; - protected int mNumberOfLines = UNSET; - protected int mFontSize = UNSET; - protected float mFontSizeInput = UNSET; - protected float mLineHeightInput = UNSET; + protected int mNumberOfLines = IntConstants.UNSET; + protected int mFontSize = IntConstants.UNSET; + protected float mFontSizeInput = IntConstants.UNSET; + protected float mLineHeightInput = IntConstants.UNSET; protected float mLetterSpacingInput = Float.NaN; protected int mTextAlign = Gravity.NO_GRAVITY; - // `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available. - protected int mLayoutDirection = UNSET; + // `IntConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol) + protected int mLayoutDirection = IntConstants.UNSET; @NonNull protected TextTransform mTextTransform = TextTransform.NONE; @@ -110,8 +109,8 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; - protected int mFontStyle = UNSET; - protected int mFontWeight = UNSET; + protected int mFontStyle = IntConstants.UNSET; + protected int mFontWeight = IntConstants.UNSET; /** * NB: If a font family is used that does not have a style in a certain Android version (ie. * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text @@ -235,11 +234,11 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { TextAttributeProps result = new TextAttributeProps(); - result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, UNSET)); - result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, UNSET)); + result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, IntConstants.UNSET)); + result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, IntConstants.UNSET)); result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); - result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, UNSET)); + result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, IntConstants.UNSET)); result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); result.setColor( props.hasKey(ViewProps.FOREGROUND_COLOR) @@ -355,12 +354,12 @@ public float getEffectiveLineHeight() { } private void setNumberOfLines(int numberOfLines) { - mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; + mNumberOfLines = numberOfLines == 0 ? IntConstants.UNSET : numberOfLines; } private void setLineHeight(float lineHeight) { mLineHeightInput = lineHeight; - if (lineHeight == UNSET) { + if (lineHeight == IntConstants.UNSET) { mLineHeight = Float.NaN; } else { mLineHeight = @@ -416,7 +415,7 @@ private void setAllowFontScaling(boolean allowFontScaling) { private void setFontSize(float fontSize) { mFontSizeInput = fontSize; - if (fontSize != UNSET) { + if (fontSize != IntConstants.UNSET) { fontSize = mAllowFontScaling ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize)) @@ -660,14 +659,14 @@ private void setTextShadowOffsetDy(float dy) { public static int getLayoutDirection(@Nullable String layoutDirection) { int androidLayoutDirection; if (layoutDirection == null || "undefined".equals(layoutDirection)) { - androidLayoutDirection = UNSET; + androidLayoutDirection = IntConstants.UNSET; } else if ("rtl".equals(layoutDirection)) { androidLayoutDirection = LayoutDirection.RTL; } else if ("ltr".equals(layoutDirection)) { androidLayoutDirection = LayoutDirection.LTR; } else { FLog.w(ReactConstants.TAG, "Invalid layoutDirection: " + layoutDirection); - androidLayoutDirection = UNSET; + androidLayoutDirection = IntConstants.UNSET; } return androidLayoutDirection; } 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 709d285f8c7b8f..7f70ca933371c9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -7,8 +7,6 @@ package com.facebook.react.views.text; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; - import android.content.Context; import android.graphics.Color; import android.os.Build; @@ -31,6 +29,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.PixelUtil; @@ -166,8 +165,8 @@ private static void buildSpannableFromFragmentsDuplicated( } ops.add( new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET + if (textAttributes.mFontStyle != IntConstants.UNSET + || textAttributes.mFontWeight != IntConstants.UNSET || textAttributes.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -393,10 +392,10 @@ public static long measureText( int maximumNumberOfLines = paragraphAttributes.hasKey(MAXIMUM_NUMBER_OF_LINES_KEY) ? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY) - : UNSET; + : IntConstants.UNSET; int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + maximumNumberOfLines == IntConstants.UNSET || maximumNumberOfLines == 0 ? layout.getLineCount() : Math.min(maximumNumberOfLines, layout.getLineCount()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index e99cd786f73878..9fb6c92c0d5954 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -8,7 +8,6 @@ package com.facebook.react.views.text; import static com.facebook.react.config.ReactFeatureFlags.enableTextSpannableCache; -import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; import android.graphics.Color; @@ -29,6 +28,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; @@ -183,8 +183,8 @@ private static void buildSpannableFromFragmentsDuplicated( } ops.add( new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); - if (textAttributes.mFontStyle != UNSET - || textAttributes.mFontWeight != UNSET + if (textAttributes.mFontStyle != IntConstants.UNSET + || textAttributes.mFontWeight != IntConstants.UNSET || textAttributes.mFontFamily != null) { ops.add( new SetSpanOperation( @@ -409,10 +409,10 @@ public static long measureText( int maximumNumberOfLines = paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES) ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) - : UNSET; + : IntConstants.UNSET; int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + maximumNumberOfLines == IntConstants.UNSET || maximumNumberOfLines == 0 ? layout.getLineCount() : Math.min(maximumNumberOfLines, layout.getLineCount()); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt index 3fa90ad741f671..c32a3fdaacbe1f 100644 --- 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 @@ -11,7 +11,7 @@ import android.content.Context import android.graphics.Color import android.text.* import android.view.View -import com.facebook.react.common.assets.ReactFontManager +import com.facebook.react.common.IntConstants import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.ReactAccessibilityDelegate import com.facebook.react.views.text.fragments.TextFragmentList @@ -21,7 +21,6 @@ import com.facebook.react.views.text.fragments.TextFragmentList */ internal object TextLayoutUtils { private const val INLINE_VIEW_PLACEHOLDER = "0" - private const val UNSET = ReactFontManager.TypefaceStyle.UNSET @JvmStatic fun buildSpannableFromTextFragmentList( @@ -180,7 +179,7 @@ internal object TextLayoutUtils { ) { val effectiveFontSize = textAttributeProvider.effectiveFontSize - if (effectiveFontSize != UNSET) { + if (effectiveFontSize != IntConstants.UNSET) { ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize))) } } @@ -198,7 +197,7 @@ internal object TextLayoutUtils { val fontWeight = textAttributeProvider.fontWeight val fontFamily = textAttributeProvider.fontFamily - if (fontStyle != UNSET || fontWeight != UNSET || fontFamily != null) { + if (fontStyle != IntConstants.UNSET || fontWeight != IntConstants.UNSET || fontFamily != null) { ops.add( SetSpanOperation( start, end, CustomStyleSpan( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 1c7950e4b3c006..014bd48093cc20 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -46,6 +46,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.StateWrapper; @@ -96,8 +97,6 @@ public class ReactEditText extends AppCompatEditText { /** A count of events sent to JS or C++. */ protected int mNativeEventCount; - private static final int UNSET = -1; - private @Nullable ArrayList mListeners; private @Nullable TextWatcherDelegator mTextWatcherDelegator; private int mStagedInputType; @@ -114,8 +113,8 @@ public class ReactEditText extends AppCompatEditText { private TextAttributes mTextAttributes; private boolean mTypefaceDirty = false; private @Nullable String mFontFamily = null; - private int mFontWeight = UNSET; - private int mFontStyle = UNSET; + private int mFontWeight = IntConstants.UNSET; + private int mFontStyle = IntConstants.UNSET; private boolean mAutoFocus = false; private boolean mDidAttachToWindow = false; private @Nullable String mPlaceholder = null; @@ -379,7 +378,7 @@ public void maybeSetSelection(int eventCounter, int start, int end) { return; } - if (start != UNSET && end != UNSET) { + if (start != IntConstants.UNSET && end != IntConstants.UNSET) { // clamp selection values for safety start = clampToTextLength(start); end = clampToTextLength(end); @@ -588,8 +587,8 @@ public void maybeUpdateTypeface() { // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything // nonstandard - if (mFontStyle != UNSET - || mFontWeight != UNSET + if (mFontStyle != IntConstants.UNSET + || mFontWeight != IntConstants.UNSET || mFontFamily != null || getFontFeatureSettings() != null) { setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG); @@ -819,8 +818,8 @@ private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) { new CustomLetterSpacingSpan(effectiveLetterSpacing), 0, workingText.length(), spanFlags); } - if (mFontStyle != UNSET - || mFontWeight != UNSET + if (mFontStyle != IntConstants.UNSET + || mFontWeight != IntConstants.UNSET || mFontFamily != null || getFontFeatureSettings() != null) { workingText.setSpan( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index 878da5199174ca..0b904002445559 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -17,6 +17,7 @@ import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.R; +import com.facebook.react.common.IntConstants; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.uimanager.Spacing; @@ -36,7 +37,7 @@ public class ReactTextInputShadowNode extends ReactBaseTextShadowNode implements YogaMeasureFunction { - private int mMostRecentEventCount = UNSET; + private int mMostRecentEventCount = IntConstants.UNSET; private @Nullable EditText mInternalEditText; private @Nullable ReactTextInputLocalData mLocalData; @@ -108,7 +109,7 @@ public long measure( } else { editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.getEffectiveFontSize()); - if (mNumberOfLines != UNSET) { + if (mNumberOfLines != IntConstants.UNSET) { editText.setLines(mNumberOfLines); } @@ -191,7 +192,7 @@ public void setTextBreakStrategy(@Nullable String textBreakStrategy) { public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { super.onCollectExtraUpdates(uiViewOperationQueue); - if (mMostRecentEventCount != UNSET) { + if (mMostRecentEventCount != IntConstants.UNSET) { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( spannedFromShadowNode(