From fa921b3c7b289800a79196468f993a0eb0bf693f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Thu, 7 Aug 2025 09:49:46 -0700 Subject: [PATCH] Migrate `TextAttributeProps` to Kotlin (#52452) Summary: Migrate com.facebook.react.views.text.TextAttributeProps to Kotlin. ## Changelog: [Android][Changed] - Migrated TextAttributeProps to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation. Pull Request resolved: https://github.com/facebook/react-native/pull/52452 Test Plan: ```bash yarn test-android yarn android ``` Reviewed By: rshest Differential Revision: D79341238 Pulled By: cortinico fbshipit-source-id: 455c7b48f47a0cf240aaf330e1fa3674798e7237 --- .../ReactAndroid/api/ReactAndroid.api | 157 ++-- .../react/views/text/TextAttributeProps.java | 811 ------------------ .../react/views/text/TextAttributeProps.kt | 569 ++++++++++++ .../react/views/text/TextLayoutManager.kt | 75 +- 4 files changed, 677 insertions(+), 935 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 6d8d60cec62..3bda89c153e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6213,97 +6213,74 @@ public final class com/facebook/react/views/text/ReactTypefaceUtils { public static final fun parseFontWeight (Ljava/lang/String;)I } -public class com/facebook/react/views/text/TextAttributeProps { - public static final field TA_KEY_ACCESSIBILITY_ROLE S - public static final field TA_KEY_ALIGNMENT S - public static final field TA_KEY_ALLOW_FONT_SCALING S - public static final field TA_KEY_BACKGROUND_COLOR S - public static final field TA_KEY_BEST_WRITING_DIRECTION S - public static final field TA_KEY_FONT_FAMILY S - public static final field TA_KEY_FONT_SIZE S - public static final field TA_KEY_FONT_SIZE_MULTIPLIER S - public static final field TA_KEY_FONT_STYLE S - public static final field TA_KEY_FONT_VARIANT S - public static final field TA_KEY_FONT_WEIGHT S - public static final field TA_KEY_FOREGROUND_COLOR S - public static final field TA_KEY_IS_HIGHLIGHTED S - public static final field TA_KEY_LAYOUT_DIRECTION S - public static final field TA_KEY_LETTER_SPACING S - public static final field TA_KEY_LINE_BREAK_STRATEGY S - public static final field TA_KEY_LINE_HEIGHT S - public static final field TA_KEY_MAX_FONT_SIZE_MULTIPLIER S - public static final field TA_KEY_OPACITY S - public static final field TA_KEY_ROLE S - public static final field TA_KEY_TEXT_DECORATION_COLOR S - public static final field TA_KEY_TEXT_DECORATION_LINE S - public static final field TA_KEY_TEXT_DECORATION_STYLE S - public static final field TA_KEY_TEXT_SHADOW_COLOR S - public static final field TA_KEY_TEXT_SHADOW_OFFSET_DX S - public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY S - public static final field TA_KEY_TEXT_SHADOW_RADIUS S - public static final field TA_KEY_TEXT_TRANSFORM S +public final class com/facebook/react/views/text/TextAttributeProps { + public static final field Companion Lcom/facebook/react/views/text/TextAttributeProps$Companion; + public static final field TA_KEY_ACCESSIBILITY_ROLE I + public static final field TA_KEY_ALIGNMENT I + public static final field TA_KEY_ALLOW_FONT_SCALING I + public static final field TA_KEY_BACKGROUND_COLOR I + public static final field TA_KEY_BEST_WRITING_DIRECTION I + public static final field TA_KEY_FONT_FAMILY I + public static final field TA_KEY_FONT_SIZE I + public static final field TA_KEY_FONT_SIZE_MULTIPLIER I + public static final field TA_KEY_FONT_STYLE I + public static final field TA_KEY_FONT_VARIANT I + public static final field TA_KEY_FONT_WEIGHT I + public static final field TA_KEY_FOREGROUND_COLOR I + public static final field TA_KEY_IS_HIGHLIGHTED I + public static final field TA_KEY_LAYOUT_DIRECTION I + public static final field TA_KEY_LETTER_SPACING I + public static final field TA_KEY_LINE_BREAK_STRATEGY I + public static final field TA_KEY_LINE_HEIGHT I + public static final field TA_KEY_MAX_FONT_SIZE_MULTIPLIER I + public static final field TA_KEY_OPACITY I + public static final field TA_KEY_ROLE I + public static final field TA_KEY_TEXT_DECORATION_COLOR I + public static final field TA_KEY_TEXT_DECORATION_LINE I + public static final field TA_KEY_TEXT_DECORATION_STYLE I + public static final field TA_KEY_TEXT_SHADOW_COLOR I + public static final field TA_KEY_TEXT_SHADOW_OFFSET_DX I + public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY I + public static final field TA_KEY_TEXT_SHADOW_RADIUS I + public static final field TA_KEY_TEXT_TRANSFORM I public static final field UNSET I - protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; - protected field mAllowFontScaling Z - protected field mBackgroundColor I - protected field mColor I - protected field mContainsImages Z - protected field mFontFamily Ljava/lang/String; - protected field mFontFeatureSettings Ljava/lang/String; - protected field mFontSize I - protected field mFontSizeInput F - protected field mFontStyle I - protected field mFontWeight I - protected field mHeightOfTallestInlineImage F - protected field mIncludeFontPadding Z - protected field mIsBackgroundColorSet Z - protected field mIsColorSet Z - protected field mIsLineThroughTextDecorationSet Z - protected field mIsUnderlineTextDecorationSet Z - protected field mLayoutDirection I - protected field mLetterSpacingInput F - protected field mLineHeight F - protected field mLineHeightInput F - protected field mMaxFontSizeMultiplier F - protected field mNumberOfLines I - protected field mOpacity F - protected field mRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; - protected field mTextAlign I - protected field mTextShadowColor I - protected field mTextShadowOffsetDx F - protected field mTextShadowOffsetDy F - protected field mTextShadowRadius F - protected field mTextTransform Lcom/facebook/react/views/text/TextTransform; - public static fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps; - public static fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps; - public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; - public fun getBackgroundColor ()I - public fun getColor ()I - public fun getEffectiveFontSize ()I - public fun getEffectiveLetterSpacing ()F - public fun getEffectiveLineHeight ()F - public static fun getEllipsizeMode (Ljava/lang/String;)Landroid/text/TextUtils$TruncateAt; - public fun getFontFamily ()Ljava/lang/String; - public fun getFontFeatureSettings ()Ljava/lang/String; - public fun getFontStyle ()I - public fun getFontWeight ()I - public static fun getHyphenationFrequency (Ljava/lang/String;)I - public static fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I - public static fun getLayoutDirection (Ljava/lang/String;)I - public fun getLetterSpacing ()F - public fun getOpacity ()F - public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; - public static fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I - public static fun getTextBreakStrategy (Ljava/lang/String;)I - public fun getTextShadowColor ()I - public fun getTextShadowOffsetDx ()F - public fun getTextShadowOffsetDy ()F - public fun getTextShadowRadius ()F - public fun getTextTransform ()Lcom/facebook/react/views/text/TextTransform; - public fun isBackgroundColorSet ()Z - public fun isColorSet ()Z - public fun isLineThroughTextDecorationSet ()Z - public fun isUnderlineTextDecorationSet ()Z + public final fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole; + public final fun getAllowFontScaling ()Z + public final fun getBackgroundColor ()Ljava/lang/Integer; + public final fun getColor ()Ljava/lang/Integer; + public final fun getEffectiveLetterSpacing ()F + public final fun getEffectiveLineHeight ()F + public final fun getFontFamily ()Ljava/lang/String; + public final fun getFontFeatureSettings ()Ljava/lang/String; + public final fun getFontSize ()I + public final fun getFontStyle ()I + public final fun getFontWeight ()I + public final fun getLayoutDirection ()I + public final fun getLetterSpacing ()F + public final fun getLineHeight ()F + public final fun getMaxFontSizeMultiplier ()F + public final fun getNumberOfLines ()I + public final fun getOpacity ()F + public final fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role; + public final fun getTextShadowColor ()I + public final fun getTextShadowOffsetDx ()F + public final fun getTextShadowOffsetDy ()F + public final fun getTextShadowRadius ()F + public final fun isBackgroundColorSet ()Z + public final fun isColorSet ()Z + public final fun isLineThroughTextDecorationSet ()Z + public final fun isUnderlineTextDecorationSet ()Z +} + +public final class com/facebook/react/views/text/TextAttributeProps$Companion { + public final fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps; + public final fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps; + public final fun getEllipsizeMode (Ljava/lang/String;)Landroid/text/TextUtils$TruncateAt; + public final fun getHyphenationFrequency (Ljava/lang/String;)I + public final fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I + public final fun getLayoutDirection (Ljava/lang/String;)I + public final fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I + public final fun getTextBreakStrategy (Ljava/lang/String;)I } public final class com/facebook/react/views/text/TextAttributes { 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 deleted file mode 100644 index 67baca4e116..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ /dev/null @@ -1,811 +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.os.Build; -import android.text.Layout; -import android.text.TextUtils; -import android.util.LayoutDirection; -import android.view.Gravity; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.mapbuffer.MapBuffer; -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 java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -// TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents -// TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to -// TextAttributeProps (e.g. TextAlign) -@Nullsafe(Nullsafe.Mode.LOCAL) -public class TextAttributeProps { - - // constants for Text Attributes serialization - public static final short TA_KEY_FOREGROUND_COLOR = 0; - public static final short TA_KEY_BACKGROUND_COLOR = 1; - public static final short TA_KEY_OPACITY = 2; - public static final short TA_KEY_FONT_FAMILY = 3; - public static final short TA_KEY_FONT_SIZE = 4; - public static final short TA_KEY_FONT_SIZE_MULTIPLIER = 5; - public static final short TA_KEY_FONT_WEIGHT = 6; - public static final short TA_KEY_FONT_STYLE = 7; - public static final short TA_KEY_FONT_VARIANT = 8; - public static final short TA_KEY_ALLOW_FONT_SCALING = 9; - public static final short TA_KEY_LETTER_SPACING = 10; - public static final short TA_KEY_LINE_HEIGHT = 11; - public static final short TA_KEY_ALIGNMENT = 12; - public static final short TA_KEY_BEST_WRITING_DIRECTION = 13; - public static final short TA_KEY_TEXT_DECORATION_COLOR = 14; - public static final short TA_KEY_TEXT_DECORATION_LINE = 15; - public static final short TA_KEY_TEXT_DECORATION_STYLE = 16; - public static final short TA_KEY_TEXT_SHADOW_RADIUS = 18; - public static final short TA_KEY_TEXT_SHADOW_COLOR = 19; - public static final short TA_KEY_TEXT_SHADOW_OFFSET_DX = 20; - public static final short TA_KEY_TEXT_SHADOW_OFFSET_DY = 21; - public static final short TA_KEY_IS_HIGHLIGHTED = 22; - public static final short TA_KEY_LAYOUT_DIRECTION = 23; - public static final short TA_KEY_ACCESSIBILITY_ROLE = 24; - public static final short TA_KEY_LINE_BREAK_STRATEGY = 25; - public static final short TA_KEY_ROLE = 26; - public static final short TA_KEY_TEXT_TRANSFORM = 27; - public static final short TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; - - 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"; - private static final String PROP_SHADOW_RADIUS = "textShadowRadius"; - private static final String PROP_SHADOW_COLOR = "textShadowColor"; - - private static final String PROP_TEXT_TRANSFORM = "textTransform"; - - private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000; - private static final int DEFAULT_JUSTIFICATION_MODE = - (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE; - private static final int DEFAULT_BREAK_STRATEGY = Layout.BREAK_STRATEGY_HIGH_QUALITY; - private static final int DEFAULT_HYPHENATION_FREQUENCY = Layout.HYPHENATION_FREQUENCY_NONE; - - protected float mLineHeight = Float.NaN; - protected boolean mIsColorSet = false; - protected boolean mAllowFontScaling = true; - protected float mMaxFontSizeMultiplier = Float.NaN; - protected int mColor; - protected boolean mIsBackgroundColorSet = false; - protected int mBackgroundColor; - protected float mOpacity = Float.NaN; - - protected int mNumberOfLines = ReactConstants.UNSET; - protected int mFontSize = ReactConstants.UNSET; - protected float mFontSizeInput = ReactConstants.UNSET; - protected float mLineHeightInput = ReactConstants.UNSET; - protected float mLetterSpacingInput = Float.NaN; - protected int mTextAlign = Gravity.NO_GRAVITY; - - // `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol) - protected int mLayoutDirection = ReactConstants.UNSET; - - @NonNull protected TextTransform mTextTransform = TextTransform.NONE; - - protected float mTextShadowOffsetDx = 0; - protected float mTextShadowOffsetDy = 0; - protected float mTextShadowRadius = 0; - protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR; - - protected boolean mIsUnderlineTextDecorationSet = false; - protected boolean mIsLineThroughTextDecorationSet = false; - protected boolean mIncludeFontPadding = true; - - protected @Nullable AccessibilityRole mAccessibilityRole = null; - protected @Nullable Role mRole = null; - - protected int mFontStyle = ReactConstants.UNSET; - protected int mFontWeight = ReactConstants.UNSET; - - /** - * NB: If a font family is used that does not have a style in a certain Android version (ie. - * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text - * nodes. To retain that style, you have to add it to those nodes explicitly. - * - *

Example, Android 4.4: - * - *

-   * Bold Text
-   *   Bold Text
-   *     Bold Text
-   *
-   * Not Bold Text
-   *   Not Bold Text
-   *     Not Bold Text
-   *
-   * Not Bold Text
-   *   Bold Text
-   *     Bold Text
-   * 
- */ - protected @Nullable String mFontFamily = null; - - /** - * @see android.graphics.Paint#setFontFeatureSettings - */ - protected @Nullable String mFontFeatureSettings = null; - - protected boolean mContainsImages = false; - protected float mHeightOfTallestInlineImage = Float.NaN; - - private TextAttributeProps() {} - - /** Build a TextAttributeProps using data from the {@link MapBuffer} received as a parameter. */ - public static TextAttributeProps fromMapBuffer(MapBuffer props) { - TextAttributeProps result = new TextAttributeProps(); - - // TODO T83483191: Review constants that are not being set! - Iterator iterator = props.iterator(); - while (iterator.hasNext()) { - MapBuffer.Entry entry = iterator.next(); - switch (entry.getKey()) { - case TA_KEY_FOREGROUND_COLOR: - result.setColor(entry.getIntValue()); - break; - case TA_KEY_BACKGROUND_COLOR: - result.setBackgroundColor(entry.getIntValue()); - break; - case TA_KEY_OPACITY: - result.setOpacity((float) entry.getDoubleValue()); - break; - case TA_KEY_FONT_FAMILY: - result.setFontFamily(entry.getStringValue()); - break; - case TA_KEY_FONT_SIZE: - result.setFontSize((float) entry.getDoubleValue()); - break; - case TA_KEY_FONT_SIZE_MULTIPLIER: - break; - case TA_KEY_FONT_WEIGHT: - result.setFontWeight(entry.getStringValue()); - break; - case TA_KEY_FONT_STYLE: - result.setFontStyle(entry.getStringValue()); - break; - case TA_KEY_FONT_VARIANT: - result.setFontVariant(entry.getMapBufferValue()); - break; - case TA_KEY_ALLOW_FONT_SCALING: - result.setAllowFontScaling(entry.getBooleanValue()); - break; - case TA_KEY_LETTER_SPACING: - result.setLetterSpacing((float) entry.getDoubleValue()); - break; - case TA_KEY_LINE_HEIGHT: - result.setLineHeight((float) entry.getDoubleValue()); - break; - case TA_KEY_ALIGNMENT: - break; - case TA_KEY_BEST_WRITING_DIRECTION: - break; - case TA_KEY_TEXT_DECORATION_COLOR: - break; - case TA_KEY_TEXT_DECORATION_LINE: - result.setTextDecorationLine(entry.getStringValue()); - break; - case TA_KEY_TEXT_DECORATION_STYLE: - break; - case TA_KEY_TEXT_SHADOW_RADIUS: - result.setTextShadowRadius((float) entry.getDoubleValue()); - break; - case TA_KEY_TEXT_SHADOW_COLOR: - result.setTextShadowColor(entry.getIntValue()); - break; - case TA_KEY_TEXT_SHADOW_OFFSET_DX: - result.setTextShadowOffsetDx((float) entry.getDoubleValue()); - break; - case TA_KEY_TEXT_SHADOW_OFFSET_DY: - result.setTextShadowOffsetDy((float) entry.getDoubleValue()); - break; - case TA_KEY_IS_HIGHLIGHTED: - break; - case TA_KEY_LAYOUT_DIRECTION: - result.setLayoutDirection(entry.getStringValue()); - break; - case TA_KEY_ACCESSIBILITY_ROLE: - result.setAccessibilityRole(entry.getStringValue()); - break; - case TA_KEY_ROLE: - result.setRole(Role.values()[entry.getIntValue()]); - break; - case TA_KEY_TEXT_TRANSFORM: - result.setTextTransform(entry.getStringValue()); - break; - case TA_KEY_MAX_FONT_SIZE_MULTIPLIER: - result.setMaxFontSizeMultiplier((float) entry.getDoubleValue()); - break; - } - } - - // TODO T83483191: Review why the following props are not serialized: - // setNumberOfLines - // setColor - // setIncludeFontPadding - return result; - } - - public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { - TextAttributeProps result = new TextAttributeProps(); - result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, ReactConstants.UNSET)); - result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET)); - result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); - result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); - result.setMaxFontSizeMultiplier( - getFloatProp(props, ViewProps.MAX_FONT_SIZE_MULTIPLIER, Float.NaN)); - result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET)); - result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); - result.setColor( - props.hasKey(ViewProps.FOREGROUND_COLOR) - ? props.getInt(ViewProps.FOREGROUND_COLOR, 0) - : null); - result.setBackgroundColor( - props.hasKey(ViewProps.BACKGROUND_COLOR) - ? props.getInt(ViewProps.BACKGROUND_COLOR, 0) - : null); - result.setOpacity(getFloatProp(props, ViewProps.OPACITY, Float.NaN)); - result.setFontFamily(getStringProp(props, ViewProps.FONT_FAMILY)); - result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT)); - result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE)); - result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)); - result.setIncludeFontPadding(getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true)); - result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE)); - result.setTextShadowOffset( - props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null); - result.setTextShadowRadius(getFloatProp(props, PROP_SHADOW_RADIUS, 1)); - result.setTextShadowColor(getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); - result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM)); - result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION)); - result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE)); - result.setRole(getStringProp(props, ViewProps.ROLE)); - return result; - } - - public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL, int defaultValue) { - if (!props.hasKey(ViewProps.TEXT_ALIGN)) { - return defaultValue; - } - - String textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN); - if ("justify".equals(textAlignPropValue)) { - return Gravity.LEFT; - } else { - if (textAlignPropValue == null || "auto".equals(textAlignPropValue)) { - return Gravity.NO_GRAVITY; - } else if ("left".equals(textAlignPropValue)) { - return isRTL ? Gravity.RIGHT : Gravity.LEFT; - } else if ("right".equals(textAlignPropValue)) { - return isRTL ? Gravity.LEFT : Gravity.RIGHT; - } else if ("center".equals(textAlignPropValue)) { - return Gravity.CENTER_HORIZONTAL; - } else { - FLog.w(ReactConstants.TAG, "Invalid textAlign: " + textAlignPropValue); - return Gravity.NO_GRAVITY; - } - } - } - - public static int getJustificationMode(ReactStylesDiffMap props, int defaultValue) { - if (!props.hasKey(ViewProps.TEXT_ALIGN)) { - return defaultValue; - } - - String textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN); - if ("justify".equals(textAlignPropValue) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return Layout.JUSTIFICATION_MODE_INTER_WORD; - } - return DEFAULT_JUSTIFICATION_MODE; - } - - private static boolean getBooleanProp( - ReactStylesDiffMap mProps, String name, boolean defaultValue) { - if (mProps.hasKey(name)) { - return mProps.getBoolean(name, defaultValue); - } else { - return defaultValue; - } - } - - private static @Nullable String getStringProp(ReactStylesDiffMap mProps, String name) { - if (mProps.hasKey(name)) { - return mProps.getString(name); - } else { - return null; - } - } - - private static int getIntProp(ReactStylesDiffMap mProps, String name, int defaultvalue) { - if (mProps.hasKey(name)) { - return mProps.getInt(name, defaultvalue); - } else { - return defaultvalue; - } - } - - private static float getFloatProp(ReactStylesDiffMap mProps, String name, float defaultvalue) { - if (mProps.hasKey(name)) { - return mProps.getFloat(name, defaultvalue); - } else { - return defaultvalue; - } - } - - private static @Nullable ReadableArray getArrayProp(ReactStylesDiffMap mProps, String name) { - if (mProps.hasKey(name)) { - return mProps.getArray(name); - } else { - return null; - } - } - - // Returns a line height which takes into account the requested line height - // and the height of the inline images. - public float getEffectiveLineHeight() { - boolean useInlineViewHeight = - !Float.isNaN(mLineHeight) - && !Float.isNaN(mHeightOfTallestInlineImage) - && mHeightOfTallestInlineImage > mLineHeight; - return useInlineViewHeight ? mHeightOfTallestInlineImage : mLineHeight; - } - - private void setNumberOfLines(int numberOfLines) { - mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines; - } - - private void setLineHeight(float lineHeight) { - mLineHeightInput = lineHeight; - if (lineHeight == ReactConstants.UNSET) { - mLineHeight = Float.NaN; - } else { - mLineHeight = - mAllowFontScaling - ? PixelUtil.toPixelFromSP(lineHeight) - : PixelUtil.toPixelFromDIP(lineHeight); - } - } - - private void setLetterSpacing(float letterSpacing) { - mLetterSpacingInput = letterSpacing; - } - - @NonNull - public TextTransform getTextTransform() { - return mTextTransform; - } - - public float getLetterSpacing() { - float letterSpacingPixels = - mAllowFontScaling - ? PixelUtil.toPixelFromSP(mLetterSpacingInput) - : PixelUtil.toPixelFromDIP(mLetterSpacingInput); - - if (mFontSize <= 0) { - throw new IllegalArgumentException( - "FontSize should be a positive value. Current value: " + mFontSize); - } - // `letterSpacingPixels` and `mFontSize` are both in pixels, - // yielding an accurate em value. - return letterSpacingPixels / mFontSize; - } - - public float getEffectiveLetterSpacing() { - return getLetterSpacing(); - } - - public int getEffectiveFontSize() { - return mFontSize; - } - - private void setAllowFontScaling(boolean allowFontScaling) { - if (allowFontScaling != mAllowFontScaling) { - mAllowFontScaling = allowFontScaling; - setFontSize(mFontSizeInput); - setLineHeight(mLineHeightInput); - } - } - - private void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) { - if (maxFontSizeMultiplier != mMaxFontSizeMultiplier) { - mMaxFontSizeMultiplier = maxFontSizeMultiplier; - setFontSize(mFontSizeInput); - setLineHeight(mLineHeightInput); - } - } - - private void setFontSize(float fontSize) { - mFontSizeInput = fontSize; - if (fontSize != ReactConstants.UNSET) { - fontSize = - mAllowFontScaling - ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize, mMaxFontSizeMultiplier)) - : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize)); - } - mFontSize = (int) fontSize; - } - - public int getColor() { - return mColor; - } - - private void setColor(@Nullable Integer color) { - mIsColorSet = (color != null); - if (color != null) { - mColor = color; - } - } - - public boolean isColorSet() { - return mIsColorSet; - } - - public int getBackgroundColor() { - return mBackgroundColor; - } - - private void setBackgroundColor(@Nullable Integer color) { - // TODO: Don't apply background color to anchor TextView since it will be applied on the View - // directly - // if (!isVirtualAnchor()) { - mIsBackgroundColorSet = (color != null); - if (color != null) { - mBackgroundColor = color; - } - // } - } - - public float getOpacity() { - return mOpacity; - } - - private void setOpacity(float opacity) { - mOpacity = opacity; - } - - public boolean isBackgroundColorSet() { - return mIsBackgroundColorSet; - } - - public int getFontStyle() { - return mFontStyle; - } - - public @Nullable String getFontFamily() { - return mFontFamily; - } - - private void setFontFamily(@Nullable String fontFamily) { - mFontFamily = fontFamily; - } - - private void setFontVariant(@Nullable ReadableArray fontVariant) { - mFontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariant); - } - - private void setFontVariant(@Nullable MapBuffer fontVariant) { - if (fontVariant == null || fontVariant.getCount() == 0) { - mFontFeatureSettings = null; - return; - } - - List features = new ArrayList<>(); - Iterator iterator = fontVariant.iterator(); - while (iterator.hasNext()) { - MapBuffer.Entry entry = iterator.next(); - String value = entry.getStringValue(); - if (value != null) { - switch (value) { - case "small-caps": - features.add("'smcp'"); - break; - case "oldstyle-nums": - features.add("'onum'"); - break; - case "lining-nums": - features.add("'lnum'"); - break; - case "tabular-nums": - features.add("'tnum'"); - break; - case "proportional-nums": - features.add("'pnum'"); - break; - case "stylistic-one": - features.add("'ss01'"); - break; - case "stylistic-two": - features.add("'ss02'"); - break; - case "stylistic-three": - features.add("'ss03'"); - break; - case "stylistic-four": - features.add("'ss04'"); - break; - case "stylistic-five": - features.add("'ss05'"); - break; - case "stylistic-six": - features.add("'ss06'"); - break; - case "stylistic-seven": - features.add("'ss07'"); - break; - case "stylistic-eight": - features.add("'ss08'"); - break; - case "stylistic-nine": - features.add("'ss09'"); - break; - case "stylistic-ten": - features.add("'ss10'"); - break; - case "stylistic-eleven": - features.add("'ss11'"); - break; - case "stylistic-twelve": - features.add("'ss12'"); - break; - case "stylistic-thirteen": - features.add("'ss13'"); - break; - case "stylistic-fourteen": - features.add("'ss14'"); - break; - case "stylistic-fifteen": - features.add("'ss15'"); - break; - case "stylistic-sixteen": - features.add("'ss16'"); - break; - case "stylistic-seventeen": - features.add("'ss17'"); - break; - case "stylistic-eighteen": - features.add("'ss18'"); - break; - case "stylistic-nineteen": - features.add("'ss19'"); - break; - case "stylistic-twenty": - features.add("'ss20'"); - break; - } - } - } - mFontFeatureSettings = TextUtils.join(", ", features); - } - - public @Nullable String getFontFeatureSettings() { - return mFontFeatureSettings; - } - - public int getFontWeight() { - return mFontWeight; - } - - private void setFontWeight(@Nullable String fontWeightString) { - mFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString); - } - - private void setFontStyle(@Nullable String fontStyleString) { - mFontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString); - } - - private void setIncludeFontPadding(boolean includepad) { - mIncludeFontPadding = includepad; - } - - private void setTextDecorationLine(@Nullable String textDecorationLineString) { - mIsUnderlineTextDecorationSet = false; - mIsLineThroughTextDecorationSet = false; - if (textDecorationLineString != null) { - for (String textDecorationLineSubString : textDecorationLineString.split("-")) { - if ("underline".equals(textDecorationLineSubString)) { - mIsUnderlineTextDecorationSet = true; - } else if ("strikethrough".equals(textDecorationLineSubString)) { - mIsLineThroughTextDecorationSet = true; - } - } - } - } - - public boolean isUnderlineTextDecorationSet() { - return mIsUnderlineTextDecorationSet; - } - - public boolean isLineThroughTextDecorationSet() { - return mIsLineThroughTextDecorationSet; - } - - private void setTextShadowOffset(@Nullable ReadableMap offsetMap) { - mTextShadowOffsetDx = 0; - mTextShadowOffsetDy = 0; - - if (offsetMap != null) { - if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH) - && !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) { - mTextShadowOffsetDx = - PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH)); - } - if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT) - && !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) { - mTextShadowOffsetDy = - PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT)); - } - } - } - - public float getTextShadowOffsetDx() { - return mTextShadowOffsetDx; - } - - private void setTextShadowOffsetDx(float dx) { - mTextShadowOffsetDx = PixelUtil.toPixelFromDIP(dx); - } - - public float getTextShadowOffsetDy() { - return mTextShadowOffsetDy; - } - - private void setTextShadowOffsetDy(float dy) { - mTextShadowOffsetDy = PixelUtil.toPixelFromDIP(dy); - } - - public static int getLayoutDirection(@Nullable String layoutDirection) { - int androidLayoutDirection; - if (layoutDirection == null || "undefined".equals(layoutDirection)) { - androidLayoutDirection = ReactConstants.UNSET; - } else if ("rtl".equals(layoutDirection)) { - androidLayoutDirection = LayoutDirection.RTL; - } else if ("ltr".equals(layoutDirection)) { - androidLayoutDirection = LayoutDirection.LTR; - } else { - FLog.w(ReactConstants.TAG, "Invalid layoutDirection: " + layoutDirection); - androidLayoutDirection = ReactConstants.UNSET; - } - return androidLayoutDirection; - } - - private void setLayoutDirection(@Nullable String layoutDirection) { - mLayoutDirection = getLayoutDirection(layoutDirection); - } - - public float getTextShadowRadius() { - return mTextShadowRadius; - } - - private void setTextShadowRadius(float textShadowRadius) { - if (textShadowRadius != mTextShadowRadius) { - mTextShadowRadius = textShadowRadius; - } - } - - public int getTextShadowColor() { - return mTextShadowColor; - } - - private void setTextShadowColor(int textShadowColor) { - if (textShadowColor != mTextShadowColor) { - mTextShadowColor = textShadowColor; - } - } - - private void setTextTransform(@Nullable String textTransform) { - if (textTransform == null || "none".equals(textTransform)) { - mTextTransform = TextTransform.NONE; - } else if ("uppercase".equals(textTransform)) { - mTextTransform = TextTransform.UPPERCASE; - } else if ("lowercase".equals(textTransform)) { - mTextTransform = TextTransform.LOWERCASE; - } else if ("capitalize".equals(textTransform)) { - mTextTransform = TextTransform.CAPITALIZE; - } else { - FLog.w(ReactConstants.TAG, "Invalid textTransform: " + textTransform); - mTextTransform = TextTransform.NONE; - } - } - - public @Nullable AccessibilityRole getAccessibilityRole() { - return mAccessibilityRole; - } - - private void setAccessibilityRole(@Nullable String accessibilityRole) { - if (accessibilityRole == null) { - mAccessibilityRole = null; - } else { - mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole); - } - } - - @Nullable - public Role getRole() { - return mRole; - } - - private void setRole(@Nullable String role) { - if (role == null) { - mRole = null; - } else { - mRole = Role.fromValue(role); - } - } - - private void setRole(Role role) { - mRole = role; - } - - public static int getTextBreakStrategy(@Nullable String textBreakStrategy) { - int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY; - if (textBreakStrategy != null) { - switch (textBreakStrategy) { - case "simple": - androidTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; - break; - case "balanced": - androidTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED; - break; - default: - androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; - break; - } - } - return androidTextBreakStrategy; - } - - public static int getHyphenationFrequency(@Nullable String hyphenationFrequency) { - int androidHyphenationFrequency = DEFAULT_HYPHENATION_FREQUENCY; - if (hyphenationFrequency != null) { - switch (hyphenationFrequency) { - case "none": - androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; - break; - case "normal": - androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL; - break; - default: - androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_FULL; - break; - } - } - return androidHyphenationFrequency; - } - - public static @Nullable TextUtils.TruncateAt getEllipsizeMode(@Nullable String ellipsizeMode) { - @Nullable TextUtils.TruncateAt truncateAt = null; - if (ellipsizeMode != null) { - switch (ellipsizeMode) { - case "head": - truncateAt = TextUtils.TruncateAt.START; - break; - case "middle": - truncateAt = TextUtils.TruncateAt.MIDDLE; - break; - case "tail": - truncateAt = TextUtils.TruncateAt.END; - break; - case "clip": - truncateAt = null; - } - } - return truncateAt; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt new file mode 100644 index 00000000000..4d6e8665be1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt @@ -0,0 +1,569 @@ +/* + * 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.os.Build +import android.text.Layout +import android.text.TextUtils +import android.text.TextUtils.TruncateAt +import android.util.LayoutDirection +import android.view.Gravity +import com.facebook.common.logging.FLog +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.mapbuffer.MapBuffer +import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP +import com.facebook.react.uimanager.PixelUtil.toPixelFromSP +import com.facebook.react.uimanager.ReactAccessibilityDelegate +import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ViewProps +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight +import kotlin.math.ceil + +// 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 private constructor() { + public var lineHeight: Float = Float.NaN + private set(value) { + lineHeightInput = value + field = + if (value == ReactConstants.UNSET.toFloat()) { + Float.NaN + } else { + if (allowFontScaling) toPixelFromSP(value) else toPixelFromDIP(value) + } + } + + public var isColorSet: Boolean = false + private set + + public var allowFontScaling: Boolean = true + private set(value) { + if (value != field) { + field = value + setFontSize(fontSizeInput) + lineHeight = lineHeightInput + } + } + + public var maxFontSizeMultiplier: Float = Float.NaN + private set(value) { + if (value != field) { + field = value + setFontSize(fontSizeInput) + lineHeight = lineHeightInput + } + } + + public var isBackgroundColorSet: Boolean = false + private set + + public var opacity: Float = Float.NaN + private set + + public var numberOfLines: Int = ReactConstants.UNSET + private set + + public var fontSize: Int = ReactConstants.UNSET + private set + + private var fontSizeInput: Float = ReactConstants.UNSET.toFloat() + private var lineHeightInput: Float = ReactConstants.UNSET.toFloat() + private var letterSpacingInput: Float = Float.NaN + + // `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol) + public var layoutDirection: Int = ReactConstants.UNSET + private set + + internal var textTransform: TextTransform = TextTransform.NONE + + public var isUnderlineTextDecorationSet: Boolean = false + private set + + public var isLineThroughTextDecorationSet: Boolean = false + private set + + private var includeFontPadding: Boolean = true + + public var accessibilityRole: AccessibilityRole? = null + private set + + public var role: ReactAccessibilityDelegate.Role? = null + private set + + public var fontStyle: Int = ReactConstants.UNSET + private set + + public var fontWeight: Int = ReactConstants.UNSET + private set + + /** + * NB: If a font family is used that does not have a style in a certain Android version (ie. + * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text + * nodes. To retain that style, you have to add it to those nodes explicitly. + * + * Example, Android 4.4: + *
+   * Bold Text
+   * Bold Text
+   * Bold Text
+   *
+   * Not Bold Text
+   * Not Bold Text
+   * Not Bold Text
+   *
+   * Not Bold Text
+   * Bold Text
+   * Bold Text
+   * 
* + */ + public var fontFamily: String? = null + private set + + /** @see android.graphics.Paint.setFontFeatureSettings */ + public var fontFeatureSettings: String? = null + private set + + private var heightOfTallestInlineImage: Float = Float.NaN + + public val effectiveLineHeight: Float + // Returns a line height which takes into account the requested line height + get() { + val useInlineViewHeight = + !lineHeight.isNaN() && + !heightOfTallestInlineImage.isNaN() && + heightOfTallestInlineImage > lineHeight + return if (useInlineViewHeight) heightOfTallestInlineImage else lineHeight + } + + private fun setNumberOfLines(numberOfLines: Int) { + this.numberOfLines = if (numberOfLines == 0) ReactConstants.UNSET else numberOfLines + } + + public var letterSpacing: Float + get() { + val letterSpacingPixels = + if (allowFontScaling) toPixelFromSP(letterSpacingInput) + else toPixelFromDIP(letterSpacingInput) + + require(fontSize > 0) { "FontSize should be a positive value. Current value: $fontSize" } + // `letterSpacingPixels` and `fontSize` are both in pixels, + // yielding an accurate em value. + return letterSpacingPixels / fontSize + } + private set(letterSpacing) { + letterSpacingInput = letterSpacing + } + + public val effectiveLetterSpacing: Float + get() = letterSpacing + + private fun setFontSize(fontSize: Float) { + var fontSizeLocal = fontSize + fontSizeInput = fontSizeLocal + if (fontSizeLocal != ReactConstants.UNSET.toFloat()) { + fontSizeLocal = + if (allowFontScaling) + ceil(toPixelFromSP(fontSize, maxFontSizeMultiplier).toDouble()).toFloat() + else ceil(toPixelFromDIP(fontSize).toDouble()).toFloat() + } + this.fontSize = fontSizeLocal.toInt() + } + + public var color: Int? = null + private set(value) { + isColorSet = (value != null) + if (value != null) { + field = value + } + } + + public var backgroundColor: Int? = 0 + private set(color) { + // TODO: Don't apply background color to anchor TextView since it will be applied on the + // View directly + // if (!isVirtualAnchor()) { + isBackgroundColorSet = (color != null) + if (color != null) { + field = color + } + // } + } + + private fun setFontVariant(fontVariant: ReadableArray?) { + fontFeatureSettings = parseFontVariant(fontVariant) + } + + private fun setFontVariant(fontVariant: MapBuffer?) { + if (fontVariant == null || fontVariant.count == 0) { + fontFeatureSettings = null + return + } + + val features: MutableList = ArrayList() + val iterator = fontVariant.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val value = entry.stringValue + @Suppress("SENSELESS_COMPARISON") + if (value != null) { + when (value) { + "small-caps" -> features.add("'smcp'") + "oldstyle-nums" -> features.add("'onum'") + "lining-nums" -> features.add("'lnum'") + "tabular-nums" -> features.add("'tnum'") + "proportional-nums" -> features.add("'pnum'") + "stylistic-one" -> features.add("'ss01'") + "stylistic-two" -> features.add("'ss02'") + "stylistic-three" -> features.add("'ss03'") + "stylistic-four" -> features.add("'ss04'") + "stylistic-five" -> features.add("'ss05'") + "stylistic-six" -> features.add("'ss06'") + "stylistic-seven" -> features.add("'ss07'") + "stylistic-eight" -> features.add("'ss08'") + "stylistic-nine" -> features.add("'ss09'") + "stylistic-ten" -> features.add("'ss10'") + "stylistic-eleven" -> features.add("'ss11'") + "stylistic-twelve" -> features.add("'ss12'") + "stylistic-thirteen" -> features.add("'ss13'") + "stylistic-fourteen" -> features.add("'ss14'") + "stylistic-fifteen" -> features.add("'ss15'") + "stylistic-sixteen" -> features.add("'ss16'") + "stylistic-seventeen" -> features.add("'ss17'") + "stylistic-eighteen" -> features.add("'ss18'") + "stylistic-nineteen" -> features.add("'ss19'") + "stylistic-twenty" -> features.add("'ss20'") + } + } + } + fontFeatureSettings = TextUtils.join(", ", features) + } + + private fun setFontWeight(fontWeightString: String?) { + fontWeight = parseFontWeight(fontWeightString) + } + + private fun setFontStyle(fontStyleString: String?) { + fontStyle = parseFontStyle(fontStyleString) + } + + private fun setTextDecorationLine(textDecorationLineString: String?) { + isUnderlineTextDecorationSet = false + isLineThroughTextDecorationSet = false + if (textDecorationLineString != null) { + for (textDecorationLineSubString in + textDecorationLineString + .split("-".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray()) { + if ("underline" == textDecorationLineSubString) { + isUnderlineTextDecorationSet = true + } else if ("strikethrough" == textDecorationLineSubString) { + isLineThroughTextDecorationSet = true + } + } + } + } + + private fun setTextShadowOffset(offsetMap: ReadableMap?) { + textShadowOffsetDx = 0f + textShadowOffsetDy = 0f + + if (offsetMap != null) { + if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH) && + !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) { + textShadowOffsetDx = toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH)) + } + if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT) && + !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) { + textShadowOffsetDy = toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT)) + } + } + } + + public var textShadowOffsetDx: Float = 0f + private set(value) { + field = toPixelFromDIP(value) + } + + public var textShadowOffsetDy: Float = 0f + private set(value) { + field = toPixelFromDIP(value) + } + + private fun setLayoutDirection(layoutDirection: String?) { + this.layoutDirection = getLayoutDirection(layoutDirection) + } + + public var textShadowRadius: Float = 0f + private set(value) { + if (value != field) { + field = value + } + } + + public var textShadowColor: Int = DEFAULT_TEXT_SHADOW_COLOR + private set(value) { + if (value != field) { + field = value + } + } + + private fun setTextTransform(textTransform: String?) { + this.textTransform = + when (textTransform) { + null, + "none" -> TextTransform.NONE + "uppercase" -> TextTransform.UPPERCASE + "lowercase" -> TextTransform.LOWERCASE + "capitalize" -> TextTransform.CAPITALIZE + else -> { + FLog.w(ReactConstants.TAG, "Invalid textTransform: $textTransform") + TextTransform.NONE + } + } + } + + private fun setAccessibilityRole(accessibilityRole: String?) { + this.accessibilityRole = + if (accessibilityRole == null) null else AccessibilityRole.fromValue(accessibilityRole) + } + + private fun setRole(role: String?) { + if (role == null) { + this.role = null + } else { + this.role = ReactAccessibilityDelegate.Role.fromValue(role) + } + } + + private fun setRole(role: ReactAccessibilityDelegate.Role) { + this.role = role + } + + public companion object { + // constants for Text Attributes serialization + public const val TA_KEY_FOREGROUND_COLOR: Int = 0 + public const val TA_KEY_BACKGROUND_COLOR: Int = 1 + public const val TA_KEY_OPACITY: Int = 2 + public const val TA_KEY_FONT_FAMILY: Int = 3 + public const val TA_KEY_FONT_SIZE: Int = 4 + public const val TA_KEY_FONT_SIZE_MULTIPLIER: Int = 5 + public const val TA_KEY_FONT_WEIGHT: Int = 6 + public const val TA_KEY_FONT_STYLE: Int = 7 + public const val TA_KEY_FONT_VARIANT: Int = 8 + public const val TA_KEY_ALLOW_FONT_SCALING: Int = 9 + public const val TA_KEY_LETTER_SPACING: Int = 10 + public const val TA_KEY_LINE_HEIGHT: Int = 11 + public const val TA_KEY_ALIGNMENT: Int = 12 + public const val TA_KEY_BEST_WRITING_DIRECTION: Int = 13 + public const val TA_KEY_TEXT_DECORATION_COLOR: Int = 14 + public const val TA_KEY_TEXT_DECORATION_LINE: Int = 15 + public const val TA_KEY_TEXT_DECORATION_STYLE: Int = 16 + public const val TA_KEY_TEXT_SHADOW_RADIUS: Int = 18 + public const val TA_KEY_TEXT_SHADOW_COLOR: Int = 19 + public const val TA_KEY_TEXT_SHADOW_OFFSET_DX: Int = 20 + public const val TA_KEY_TEXT_SHADOW_OFFSET_DY: Int = 21 + public const val TA_KEY_IS_HIGHLIGHTED: Int = 22 + public const val TA_KEY_LAYOUT_DIRECTION: Int = 23 + public const val TA_KEY_ACCESSIBILITY_ROLE: Int = 24 + public const val TA_KEY_LINE_BREAK_STRATEGY: Int = 25 + public const val TA_KEY_ROLE: Int = 26 + public const val TA_KEY_TEXT_TRANSFORM: Int = 27 + public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29 + + public const val UNSET: Int = -1 + + private const val PROP_SHADOW_OFFSET = "textShadowOffset" + private const val PROP_SHADOW_OFFSET_WIDTH = "width" + private const val PROP_SHADOW_OFFSET_HEIGHT = "height" + private const val PROP_SHADOW_RADIUS = "textShadowRadius" + private const val PROP_SHADOW_COLOR = "textShadowColor" + + private const val PROP_TEXT_TRANSFORM = "textTransform" + + private const val DEFAULT_TEXT_SHADOW_COLOR = 0x55000000 + private val DEFAULT_JUSTIFICATION_MODE = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) 0 else Layout.JUSTIFICATION_MODE_NONE + private const val DEFAULT_BREAK_STRATEGY = Layout.BREAK_STRATEGY_HIGH_QUALITY + private const val DEFAULT_HYPHENATION_FREQUENCY = Layout.HYPHENATION_FREQUENCY_NONE + + /** Build a TextAttributeProps using data from the [MapBuffer] received as a parameter. */ + public fun fromMapBuffer(props: MapBuffer): TextAttributeProps { + val result = TextAttributeProps() + + // TODO T83483191: Review constants that are not being set! + val iterator = props.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + when (entry.key) { + TA_KEY_FOREGROUND_COLOR -> result.color = entry.intValue + TA_KEY_BACKGROUND_COLOR -> result.backgroundColor = entry.intValue + TA_KEY_OPACITY -> result.opacity = entry.doubleValue.toFloat() + TA_KEY_FONT_FAMILY -> result.fontFamily = entry.stringValue + TA_KEY_FONT_SIZE -> result.setFontSize(entry.doubleValue.toFloat()) + TA_KEY_FONT_SIZE_MULTIPLIER -> {} + TA_KEY_FONT_WEIGHT -> result.setFontWeight(entry.stringValue) + TA_KEY_FONT_STYLE -> result.setFontStyle(entry.stringValue) + TA_KEY_FONT_VARIANT -> result.setFontVariant(entry.mapBufferValue) + TA_KEY_ALLOW_FONT_SCALING -> result.allowFontScaling = entry.booleanValue + TA_KEY_LETTER_SPACING -> result.letterSpacing = entry.doubleValue.toFloat() + TA_KEY_LINE_HEIGHT -> result.lineHeight = entry.doubleValue.toFloat() + TA_KEY_ALIGNMENT -> {} + TA_KEY_BEST_WRITING_DIRECTION -> {} + TA_KEY_TEXT_DECORATION_COLOR -> {} + TA_KEY_TEXT_DECORATION_LINE -> result.setTextDecorationLine(entry.stringValue) + TA_KEY_TEXT_DECORATION_STYLE -> {} + TA_KEY_TEXT_SHADOW_RADIUS -> result.textShadowRadius = entry.doubleValue.toFloat() + TA_KEY_TEXT_SHADOW_COLOR -> result.textShadowColor = entry.intValue + TA_KEY_TEXT_SHADOW_OFFSET_DX -> result.textShadowOffsetDx = entry.doubleValue.toFloat() + TA_KEY_TEXT_SHADOW_OFFSET_DY -> result.textShadowOffsetDy = entry.doubleValue.toFloat() + TA_KEY_IS_HIGHLIGHTED -> {} + TA_KEY_LAYOUT_DIRECTION -> result.setLayoutDirection(entry.stringValue) + TA_KEY_ACCESSIBILITY_ROLE -> result.setAccessibilityRole(entry.stringValue) + TA_KEY_ROLE -> result.setRole(ReactAccessibilityDelegate.Role.entries[entry.intValue]) + TA_KEY_TEXT_TRANSFORM -> result.setTextTransform(entry.stringValue) + TA_KEY_MAX_FONT_SIZE_MULTIPLIER -> + result.maxFontSizeMultiplier = entry.doubleValue.toFloat() + } + } + + // TODO T83483191: Review why the following props are not serialized: + // setNumberOfLines + // setColor + // setIncludeFontPadding + return result + } + + public fun fromReadableMap(props: ReactStylesDiffMap): TextAttributeProps { + val result = TextAttributeProps() + result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, ReactConstants.UNSET)) + result.lineHeight = getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET.toFloat()) + result.letterSpacing = getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN) + result.allowFontScaling = getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true) + result.maxFontSizeMultiplier = + getFloatProp(props, ViewProps.MAX_FONT_SIZE_MULTIPLIER, Float.NaN) + result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET.toFloat())) + result.color = if (props.hasKey(ViewProps.COLOR)) props.getInt(ViewProps.COLOR, 0) else null + result.color = + if (props.hasKey(ViewProps.FOREGROUND_COLOR)) props.getInt(ViewProps.FOREGROUND_COLOR, 0) + else null + result.backgroundColor = + if (props.hasKey(ViewProps.BACKGROUND_COLOR)) props.getInt(ViewProps.BACKGROUND_COLOR, 0) + else null + result.opacity = getFloatProp(props, ViewProps.OPACITY, Float.NaN) + result.fontFamily = getStringProp(props, ViewProps.FONT_FAMILY) + result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT)) + result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE)) + result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)) + result.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true) + result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE)) + result.setTextShadowOffset( + if (props.hasKey(PROP_SHADOW_OFFSET)) props.getMap(PROP_SHADOW_OFFSET) else null) + result.textShadowRadius = getFloatProp(props, PROP_SHADOW_RADIUS, 1f) + result.textShadowColor = getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR) + result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM)) + result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION)) + result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE)) + result.setRole(getStringProp(props, ViewProps.ROLE)) + return result + } + + public fun getTextAlignment(props: ReactStylesDiffMap, isRTL: Boolean, defaultValue: Int): Int { + if (!props.hasKey(ViewProps.TEXT_ALIGN)) { + return defaultValue + } + + return when (val textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN)) { + "justify" -> Gravity.LEFT + null, + "auto" -> Gravity.NO_GRAVITY + "left" -> if (isRTL) Gravity.RIGHT else Gravity.LEFT + "right" -> if (isRTL) Gravity.LEFT else Gravity.RIGHT + "center" -> Gravity.CENTER_HORIZONTAL + else -> { + FLog.w(ReactConstants.TAG, "Invalid textAlign: $textAlignPropValue") + Gravity.NO_GRAVITY + } + } + } + + public fun getJustificationMode(props: ReactStylesDiffMap, defaultValue: Int): Int { + if (!props.hasKey(ViewProps.TEXT_ALIGN)) { + return defaultValue + } + + val textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN) + if ("justify" == textAlignPropValue && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Layout.JUSTIFICATION_MODE_INTER_WORD + } + return DEFAULT_JUSTIFICATION_MODE + } + + private fun getBooleanProp( + props: ReactStylesDiffMap, + name: String, + defaultValue: Boolean + ): Boolean = if (props.hasKey(name)) props.getBoolean(name, defaultValue) else defaultValue + + private fun getStringProp(props: ReactStylesDiffMap, name: String): String? = + if (props.hasKey(name)) props.getString(name) else null + + private fun getIntProp(props: ReactStylesDiffMap, name: String, defaultValue: Int): Int = + if (props.hasKey(name)) props.getInt(name, defaultValue) else defaultValue + + private fun getFloatProp(props: ReactStylesDiffMap, name: String, defaultValue: Float): Float = + if (props.hasKey(name)) props.getFloat(name, defaultValue) else defaultValue + + private fun getArrayProp(props: ReactStylesDiffMap, name: String): ReadableArray? = + if (props.hasKey(name)) props.getArray(name) else null + + public fun getLayoutDirection(layoutDirection: String?): Int { + return when (layoutDirection) { + null, + "undefined" -> ReactConstants.UNSET + "rtl" -> LayoutDirection.RTL + "ltr" -> LayoutDirection.LTR + else -> { + FLog.w(ReactConstants.TAG, "Invalid layoutDirection: $layoutDirection") + ReactConstants.UNSET + } + } + } + + public fun getTextBreakStrategy(textBreakStrategy: String?): Int = + when (textBreakStrategy) { + null -> DEFAULT_BREAK_STRATEGY + "simple" -> Layout.BREAK_STRATEGY_SIMPLE + "balanced" -> Layout.BREAK_STRATEGY_BALANCED + else -> Layout.BREAK_STRATEGY_HIGH_QUALITY + } + + public fun getHyphenationFrequency(hyphenationFrequency: String?): Int = + when (hyphenationFrequency) { + null -> DEFAULT_HYPHENATION_FREQUENCY + "none" -> Layout.HYPHENATION_FREQUENCY_NONE + "normal" -> Layout.HYPHENATION_FREQUENCY_NORMAL + else -> Layout.HYPHENATION_FREQUENCY_FULL + } + + public fun getEllipsizeMode(ellipsizeMode: String?): TruncateAt? = + when (ellipsizeMode) { + "head" -> TruncateAt.START + "middle" -> TruncateAt.MIDDLE + "tail" -> TruncateAt.END + "clip" -> null + else -> null + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 7f66d6b0123..2a9ec5cb045 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -231,7 +231,7 @@ internal object TextLayoutManager { TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES)) sb.append( - TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform)) + TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.textTransform)) val end = sb.length val reactTag = @@ -246,10 +246,10 @@ internal object TextLayoutManager { TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt()))) } else if (end >= start) { val roleIsLink = - if (textAttributes.mRole != null) - (textAttributes.mRole == ReactAccessibilityDelegate.Role.LINK) + if (textAttributes.role != null) + (textAttributes.role == ReactAccessibilityDelegate.Role.LINK) else - (textAttributes.mAccessibilityRole == + (textAttributes.accessibilityRole == ReactAccessibilityDelegate.AccessibilityRole.LINK) if (roleIsLink) { if (ReactNativeFeatureFlags.enablePreparedTextLayout()) { @@ -258,13 +258,17 @@ internal object TextLayoutManager { ops.add(SetSpanOperation(start, end, ReactClickableSpan(reactTag))) } } - if (textAttributes.mIsColorSet) { - ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textAttributes.mColor))) + if (textAttributes.isColorSet) { + textAttributes.color + ?.let { ReactForegroundColorSpan(it) } + ?.let { SetSpanOperation(start, end, it) } + ?.let { ops.add(it) } } - if (textAttributes.mIsBackgroundColorSet) { - ops.add( - SetSpanOperation( - start, end, ReactBackgroundColorSpan(textAttributes.mBackgroundColor))) + if (textAttributes.isBackgroundColorSet) { + textAttributes.backgroundColor + ?.let { ReactBackgroundColorSpan(it) } + ?.let { SetSpanOperation(start, end, it) } + ?.let { ops.add(it) } } if (!textAttributes.opacity.isNaN()) { ops.add(SetSpanOperation(start, end, ReactOpacitySpan(textAttributes.opacity))) @@ -273,40 +277,40 @@ internal object TextLayoutManager { ops.add( SetSpanOperation(start, end, CustomLetterSpacingSpan(textAttributes.letterSpacing))) } - ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(textAttributes.mFontSize))) - if (textAttributes.mFontStyle != ReactConstants.UNSET || - textAttributes.mFontWeight != ReactConstants.UNSET || - textAttributes.mFontFamily != null) { + ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(textAttributes.fontSize))) + if (textAttributes.fontStyle != ReactConstants.UNSET || + textAttributes.fontWeight != ReactConstants.UNSET || + textAttributes.fontFamily != null) { ops.add( SetSpanOperation( start, end, CustomStyleSpan( - textAttributes.mFontStyle, - textAttributes.mFontWeight, - textAttributes.mFontFeatureSettings, - textAttributes.mFontFamily, + textAttributes.fontStyle, + textAttributes.fontWeight, + textAttributes.fontFeatureSettings, + textAttributes.fontFamily, context.assets))) } - if (textAttributes.mIsUnderlineTextDecorationSet) { + if (textAttributes.isUnderlineTextDecorationSet) { ops.add(SetSpanOperation(start, end, ReactUnderlineSpan())) } - if (textAttributes.mIsLineThroughTextDecorationSet) { + if (textAttributes.isLineThroughTextDecorationSet) { ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan())) } - if ((textAttributes.mTextShadowOffsetDx != 0f || - textAttributes.mTextShadowOffsetDy != 0f || - textAttributes.mTextShadowRadius != 0f) && - Color.alpha(textAttributes.mTextShadowColor) != 0) { + if ((textAttributes.textShadowOffsetDx != 0f || + textAttributes.textShadowOffsetDy != 0f || + textAttributes.textShadowRadius != 0f) && + Color.alpha(textAttributes.textShadowColor) != 0) { ops.add( SetSpanOperation( start, end, ShadowStyleSpan( - textAttributes.mTextShadowOffsetDx, - textAttributes.mTextShadowOffsetDy, - textAttributes.mTextShadowRadius, - textAttributes.mTextShadowColor))) + textAttributes.textShadowOffsetDx, + textAttributes.textShadowOffsetDy, + textAttributes.textShadowRadius, + textAttributes.textShadowColor))) } if (!textAttributes.effectiveLineHeight.isNaN()) { ops.add( @@ -409,12 +413,16 @@ internal object TextLayoutManager { } if (fragment.props.isColorSet) { - spannable.setSpan(ReactForegroundColorSpan(fragment.props.color), start, end, spanFlags) + spannable.setSpan( + fragment.props.color?.let { ReactForegroundColorSpan(it) }, start, end, spanFlags) } if (fragment.props.isBackgroundColorSet) { spannable.setSpan( - ReactBackgroundColorSpan(fragment.props.backgroundColor), start, end, spanFlags) + fragment.props.backgroundColor?.let { ReactBackgroundColorSpan(it) }, + start, + end, + spanFlags) } if (!fragment.props.opacity.isNaN()) { @@ -426,8 +434,7 @@ internal object TextLayoutManager { CustomLetterSpacingSpan(fragment.props.letterSpacing), start, end, spanFlags) } - // TODO: Should this be using effectiveFontSize instead of fontSize? - spannable.setSpan(ReactAbsoluteSizeSpan(fragment.props.mFontSize), start, end, spanFlags) + spannable.setSpan(ReactAbsoluteSizeSpan(fragment.props.fontSize), start, end, spanFlags) if (fragment.props.fontStyle != ReactConstants.UNSET || fragment.props.fontWeight != ReactConstants.UNSET || @@ -611,8 +618,8 @@ internal object TextLayoutManager { baseTextAttributes: TextAttributeProps, context: Context ) { - if (baseTextAttributes.effectiveFontSize != ReactConstants.UNSET) { - paint.textSize = baseTextAttributes.effectiveFontSize.toFloat() + if (baseTextAttributes.fontSize != ReactConstants.UNSET) { + paint.textSize = baseTextAttributes.fontSize.toFloat() } if (baseTextAttributes.fontStyle != ReactConstants.UNSET ||