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
This commit is contained in:
Mateo Guzmán
2025-08-07 09:49:46 -07:00
committed by Facebook GitHub Bot
parent bf13ecba7f
commit fa921b3c7b
4 changed files with 677 additions and 935 deletions
@@ -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 {
@@ -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.
*
* <p>Example, Android 4.4:
*
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
* </pre>
*/
protected @Nullable String mFontFamily = null;
/**
* @see android.graphics.Paint#setFontFeatureSettings
*/
protected @Nullable String mFontFeatureSettings = null;
protected boolean mContainsImages = false;
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<MapBuffer.Entry> 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<String> features = new ArrayList<>();
Iterator<MapBuffer.Entry> 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;
}
}
@@ -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:
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
* </pre> *
*/
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<String?> = 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
}
}
}
@@ -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 ||