mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Migrate ReactBaseTextShadowNode to Kotlin (#52449)
Summary: Migrate com.facebook.react.views.text.ReactBaseTextShadowNode to Kotlin. ## Changelog: [Android][Changed] - Migrated ReactBaseTextShadowNode 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/52449 Test Plan: ```bash yarn test-android yarn android ``` Reviewed By: mdvacca Differential Revision: D79341403 Pulled By: cortinico fbshipit-source-id: ff5dd7a8c3e0220812dd3a214d8d680ccd83f4d8
This commit is contained in:
committed by
Facebook GitHub Bot
parent
0aeb26bdfc
commit
8ccfff9a46
@@ -6012,6 +6012,7 @@ public final class com/facebook/react/views/text/DefaultStyleValuesUtil {
|
||||
}
|
||||
|
||||
public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : com/facebook/react/uimanager/LayoutShadowNode {
|
||||
public static final field Companion Lcom/facebook/react/views/text/ReactBaseTextShadowNode$Companion;
|
||||
public static final field DEFAULT_TEXT_SHADOW_COLOR I
|
||||
public static final field PROP_SHADOW_COLOR Ljava/lang/String;
|
||||
public static final field PROP_SHADOW_OFFSET Ljava/lang/String;
|
||||
@@ -6019,61 +6020,86 @@ public abstract class com/facebook/react/views/text/ReactBaseTextShadowNode : co
|
||||
public static final field PROP_SHADOW_OFFSET_WIDTH Ljava/lang/String;
|
||||
public static final field PROP_SHADOW_RADIUS Ljava/lang/String;
|
||||
public static final field PROP_TEXT_TRANSFORM Ljava/lang/String;
|
||||
protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
|
||||
protected field mAdjustsFontSizeToFit Z
|
||||
protected field mBackgroundColor I
|
||||
protected field mColor I
|
||||
protected field mContainsImages Z
|
||||
protected field mFontFamily Ljava/lang/String;
|
||||
protected field mFontFeatureSettings Ljava/lang/String;
|
||||
protected field mFontStyle I
|
||||
protected field mFontWeight I
|
||||
protected field mHyphenationFrequency I
|
||||
protected field mIncludeFontPadding Z
|
||||
protected field mInlineViews Ljava/util/Map;
|
||||
protected field mIsBackgroundColorSet Z
|
||||
protected field mIsColorSet Z
|
||||
protected field mIsLineThroughTextDecorationSet Z
|
||||
protected field mIsUnderlineTextDecorationSet Z
|
||||
protected field mJustificationMode I
|
||||
protected field mMinimumFontScale F
|
||||
protected field mNumberOfLines I
|
||||
protected field mReactTextViewManagerCallback Lcom/facebook/react/views/text/ReactTextViewManagerCallback;
|
||||
protected field mRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
|
||||
protected field mTextAlign I
|
||||
protected field mTextAttributes Lcom/facebook/react/views/text/TextAttributes;
|
||||
protected field mTextBreakStrategy I
|
||||
protected field mTextShadowColor I
|
||||
protected field mTextShadowOffsetDx F
|
||||
protected field mTextShadowOffsetDy F
|
||||
protected field mTextShadowRadius F
|
||||
public fun <init> ()V
|
||||
public fun <init> (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
|
||||
public fun setAccessibilityRole (Ljava/lang/String;)V
|
||||
public fun setAdjustFontSizeToFit (Z)V
|
||||
public fun setAllowFontScaling (Z)V
|
||||
public fun setBackgroundColor (Ljava/lang/Integer;)V
|
||||
public fun setColor (Ljava/lang/Integer;)V
|
||||
public fun setFontFamily (Ljava/lang/String;)V
|
||||
public fun setFontSize (F)V
|
||||
public fun setFontStyle (Ljava/lang/String;)V
|
||||
public fun setFontVariant (Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun setFontWeight (Ljava/lang/String;)V
|
||||
public fun setIncludeFontPadding (Z)V
|
||||
public fun setLetterSpacing (F)V
|
||||
public fun setLineHeight (F)V
|
||||
public fun setMaxFontSizeMultiplier (F)V
|
||||
public fun setMinimumFontScale (F)V
|
||||
public fun setNumberOfLines (I)V
|
||||
public fun setRole (Ljava/lang/String;)V
|
||||
public fun setTextAlign (Ljava/lang/String;)V
|
||||
public synthetic fun <init> (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
protected final fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
|
||||
protected final fun getAdjustsFontSizeToFit ()Z
|
||||
protected final fun getBackgroundColor ()I
|
||||
protected final fun getColor ()I
|
||||
protected final fun getContainsImages ()Z
|
||||
protected final fun getFontFamily ()Ljava/lang/String;
|
||||
protected final fun getFontFeatureSettings ()Ljava/lang/String;
|
||||
protected final fun getFontStyle ()I
|
||||
protected final fun getFontWeight ()I
|
||||
protected final fun getHyphenationFrequency ()I
|
||||
protected final fun getIncludeFontPadding ()Z
|
||||
protected final fun getInlineViews ()Ljava/util/Map;
|
||||
protected final fun getJustificationMode ()I
|
||||
protected final fun getMinimumFontScale ()F
|
||||
protected final fun getNumberOfLines ()I
|
||||
protected final fun getReactTextViewManagerCallback ()Lcom/facebook/react/views/text/ReactTextViewManagerCallback;
|
||||
protected final fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
|
||||
protected final fun getTextAlign ()I
|
||||
protected final fun getTextAttributes ()Lcom/facebook/react/views/text/TextAttributes;
|
||||
protected final fun getTextBreakStrategy ()I
|
||||
protected final fun getTextShadowColor ()I
|
||||
protected final fun getTextShadowOffsetDx ()F
|
||||
protected final fun getTextShadowOffsetDy ()F
|
||||
protected final fun getTextShadowRadius ()F
|
||||
protected final fun isBackgroundColorSet ()Z
|
||||
protected final fun isColorSet ()Z
|
||||
protected final fun isLineThroughTextDecorationSet ()Z
|
||||
protected final fun isUnderlineTextDecorationSet ()Z
|
||||
protected final fun setAccessibilityRole (Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;)V
|
||||
public final fun setAccessibilityRole (Ljava/lang/String;)V
|
||||
public final fun setAdjustFontSizeToFit (Z)V
|
||||
protected final fun setAdjustsFontSizeToFit (Z)V
|
||||
public final fun setAllowFontScaling (Z)V
|
||||
protected final fun setBackgroundColor (I)V
|
||||
public final fun setBackgroundColor (Ljava/lang/Integer;)V
|
||||
protected final fun setBackgroundColorSet (Z)V
|
||||
protected final fun setColor (I)V
|
||||
public final fun setColor (Ljava/lang/Integer;)V
|
||||
protected final fun setColorSet (Z)V
|
||||
protected final fun setContainsImages (Z)V
|
||||
public final fun setFontFamily (Ljava/lang/String;)V
|
||||
protected final fun setFontFeatureSettings (Ljava/lang/String;)V
|
||||
public final fun setFontSize (F)V
|
||||
protected final fun setFontStyle (I)V
|
||||
public final fun setFontStyle (Ljava/lang/String;)V
|
||||
public final fun setFontVariant (Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
protected final fun setFontWeight (I)V
|
||||
public final fun setFontWeight (Ljava/lang/String;)V
|
||||
protected final fun setHyphenationFrequency (I)V
|
||||
public final fun setIncludeFontPadding (Z)V
|
||||
protected final fun setInlineViews (Ljava/util/Map;)V
|
||||
protected final fun setJustificationMode (I)V
|
||||
public final fun setLetterSpacing (F)V
|
||||
public final fun setLineHeight (F)V
|
||||
protected final fun setLineThroughTextDecorationSet (Z)V
|
||||
public final fun setMaxFontSizeMultiplier (F)V
|
||||
public final fun setMinimumFontScale (F)V
|
||||
public final fun setNumberOfLines (I)V
|
||||
protected final fun setReactTextViewManagerCallback (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
|
||||
protected final fun setRole (Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;)V
|
||||
public final fun setRole (Ljava/lang/String;)V
|
||||
public final fun setTextAlign (Ljava/lang/String;)V
|
||||
protected final fun setTextAttributes (Lcom/facebook/react/views/text/TextAttributes;)V
|
||||
protected final fun setTextBreakStrategy (I)V
|
||||
public fun setTextBreakStrategy (Ljava/lang/String;)V
|
||||
public fun setTextDecorationLine (Ljava/lang/String;)V
|
||||
public fun setTextShadowColor (I)V
|
||||
public fun setTextShadowOffset (Lcom/facebook/react/bridge/ReadableMap;)V
|
||||
public fun setTextShadowRadius (F)V
|
||||
public fun setTextTransform (Ljava/lang/String;)V
|
||||
protected fun spannedFromShadowNode (Lcom/facebook/react/views/text/ReactBaseTextShadowNode;Ljava/lang/String;ZLcom/facebook/react/uimanager/NativeViewHierarchyOptimizer;)Landroid/text/Spannable;
|
||||
public final fun setTextDecorationLine (Ljava/lang/String;)V
|
||||
public final fun setTextShadowColor (I)V
|
||||
public final fun setTextShadowOffset (Lcom/facebook/react/bridge/ReadableMap;)V
|
||||
protected final fun setTextShadowOffsetDx (F)V
|
||||
protected final fun setTextShadowOffsetDy (F)V
|
||||
public final fun setTextShadowRadius (F)V
|
||||
public final fun setTextTransform (Ljava/lang/String;)V
|
||||
protected final fun setUnderlineTextDecorationSet (Z)V
|
||||
protected final fun spannedFromShadowNode (Lcom/facebook/react/views/text/ReactBaseTextShadowNode;Ljava/lang/String;ZLcom/facebook/react/uimanager/NativeViewHierarchyOptimizer;)Landroid/text/Spannable;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/views/text/ReactBaseTextShadowNode$Companion {
|
||||
}
|
||||
|
||||
public final class com/facebook/react/views/text/ReactFontManager {
|
||||
|
||||
-669
@@ -1,669 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.text;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
|
||||
import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.views.text.internal.ReactTextInlineImageShadowNode;
|
||||
import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan;
|
||||
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
|
||||
import com.facebook.react.views.text.internal.span.CustomStyleSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactClickableSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactTagSpan;
|
||||
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan;
|
||||
import com.facebook.react.views.text.internal.span.SetSpanOperation;
|
||||
import com.facebook.react.views.text.internal.span.ShadowStyleSpan;
|
||||
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
|
||||
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
|
||||
import com.facebook.yoga.YogaDirection;
|
||||
import com.facebook.yoga.YogaUnit;
|
||||
import com.facebook.yoga.YogaValue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link ReactShadowNode} abstract class for spannable text nodes.
|
||||
*
|
||||
* <p>This class handles all text attributes associated with {@code <Text>}-ish node. A concrete
|
||||
* node can be an anchor {@code <Text>} node, an anchor {@code <TextInput>} node or virtual {@code
|
||||
* <Text>} node inside {@code <Text>} or {@code <TextInput>} node. Or even something else.
|
||||
*
|
||||
* <p>This also node calculates {@link Spannable} object based on subnodes of the same type, which
|
||||
* can be used in concrete classes to feed native views and compute layout.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
// Use a direction weak character so the placeholder doesn't change the direction of the previous
|
||||
// character.
|
||||
// https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters
|
||||
private static final String INLINE_VIEW_PLACEHOLDER = "0";
|
||||
|
||||
public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
|
||||
public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
|
||||
public static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
|
||||
public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
|
||||
public static final String PROP_SHADOW_COLOR = "textShadowColor";
|
||||
|
||||
public static final String PROP_TEXT_TRANSFORM = "textTransform";
|
||||
|
||||
public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
|
||||
|
||||
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
|
||||
|
||||
private static void buildSpannedFromShadowNode(
|
||||
ReactBaseTextShadowNode textShadowNode,
|
||||
SpannableStringBuilder sb,
|
||||
List<SetSpanOperation> ops,
|
||||
@Nullable TextAttributes parentTextAttributes,
|
||||
boolean supportsInlineViews,
|
||||
@Nullable Map<Integer, ReactShadowNode> inlineViews,
|
||||
int start) {
|
||||
|
||||
TextAttributes textAttributes;
|
||||
if (parentTextAttributes != null) {
|
||||
textAttributes = parentTextAttributes.applyChild(textShadowNode.mTextAttributes);
|
||||
} else {
|
||||
textAttributes = textShadowNode.mTextAttributes;
|
||||
}
|
||||
|
||||
for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
|
||||
ReactShadowNode child = textShadowNode.getChildAt(i);
|
||||
|
||||
if (child instanceof ReactRawTextShadowNode) {
|
||||
String childText = ((ReactRawTextShadowNode) child).getText();
|
||||
if (childText != null) {
|
||||
sb.append(TextTransform.apply(childText, textAttributes.textTransform));
|
||||
}
|
||||
} else if (child instanceof ReactBaseTextShadowNode) {
|
||||
buildSpannedFromShadowNode(
|
||||
(ReactBaseTextShadowNode) child,
|
||||
sb,
|
||||
ops,
|
||||
textAttributes,
|
||||
supportsInlineViews,
|
||||
inlineViews,
|
||||
sb.length());
|
||||
} else if (child instanceof ReactTextInlineImageShadowNode) {
|
||||
// We make the image take up 1 character in the span and put a corresponding character into
|
||||
// the text so that the image doesn't run over any following text.
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER);
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
|
||||
sb.length(),
|
||||
((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
|
||||
} else if (supportsInlineViews) {
|
||||
int reactTag = child.getReactTag();
|
||||
YogaValue widthValue = child.getStyleWidth();
|
||||
YogaValue heightValue = child.getStyleHeight();
|
||||
|
||||
float width;
|
||||
float height;
|
||||
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
|
||||
// If the measurement of the child isn't calculated, we calculate the layout for the
|
||||
// view using Yoga
|
||||
child.calculateLayout();
|
||||
width = child.getLayoutWidth();
|
||||
height = child.getLayoutHeight();
|
||||
} else {
|
||||
width = widthValue.value;
|
||||
height = heightValue.value;
|
||||
}
|
||||
|
||||
// We make the inline view take up 1 character in the span and put a corresponding character
|
||||
// into
|
||||
// the text so that the inline view doesn't run over any following text.
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER);
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
|
||||
sb.length(),
|
||||
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
|
||||
|
||||
// supportsInlineViews is true, so we can assume that inlineViews is not null
|
||||
Assertions.assertNotNull(inlineViews).put(reactTag, child);
|
||||
} else {
|
||||
throw new IllegalViewOperationException(
|
||||
"Unexpected view type nested under a <Text> or <TextInput> node: " + child.getClass());
|
||||
}
|
||||
child.markUpdateSeen();
|
||||
}
|
||||
int end = sb.length();
|
||||
if (end >= start) {
|
||||
if (textShadowNode.mIsColorSet) {
|
||||
ops.add(
|
||||
new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor)));
|
||||
}
|
||||
if (textShadowNode.mIsBackgroundColorSet) {
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
start, end, new ReactBackgroundColorSpan(textShadowNode.mBackgroundColor)));
|
||||
}
|
||||
boolean roleIsLink =
|
||||
textShadowNode.mRole != null
|
||||
? textShadowNode.mRole == Role.LINK
|
||||
: textShadowNode.mAccessibilityRole == AccessibilityRole.LINK;
|
||||
if (roleIsLink) {
|
||||
ops.add(
|
||||
new SetSpanOperation(start, end, new ReactClickableSpan(textShadowNode.getReactTag())));
|
||||
}
|
||||
float effectiveLetterSpacing = textAttributes.getEffectiveLetterSpacing();
|
||||
if (!Float.isNaN(effectiveLetterSpacing)
|
||||
&& (parentTextAttributes == null
|
||||
|| parentTextAttributes.getEffectiveLetterSpacing() != effectiveLetterSpacing)) {
|
||||
ops.add(
|
||||
new SetSpanOperation(start, end, new CustomLetterSpacingSpan(effectiveLetterSpacing)));
|
||||
}
|
||||
int effectiveFontSize = textAttributes.getEffectiveFontSize();
|
||||
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything like
|
||||
// `Float.NaN`.
|
||||
parentTextAttributes == null
|
||||
|| parentTextAttributes.getEffectiveFontSize() != effectiveFontSize) {
|
||||
ops.add(new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(effectiveFontSize)));
|
||||
}
|
||||
if (textShadowNode.mFontStyle != ReactConstants.UNSET
|
||||
|| textShadowNode.mFontWeight != ReactConstants.UNSET
|
||||
|| textShadowNode.mFontFamily != null) {
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
start,
|
||||
end,
|
||||
new CustomStyleSpan(
|
||||
textShadowNode.mFontStyle,
|
||||
textShadowNode.mFontWeight,
|
||||
textShadowNode.mFontFeatureSettings,
|
||||
textShadowNode.mFontFamily,
|
||||
textShadowNode.getThemedContext().getAssets())));
|
||||
}
|
||||
if (textShadowNode.mIsUnderlineTextDecorationSet) {
|
||||
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
|
||||
}
|
||||
if (textShadowNode.mIsLineThroughTextDecorationSet) {
|
||||
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
|
||||
}
|
||||
if ((textShadowNode.mTextShadowOffsetDx != 0
|
||||
|| textShadowNode.mTextShadowOffsetDy != 0
|
||||
|| textShadowNode.mTextShadowRadius != 0)
|
||||
&& Color.alpha(textShadowNode.mTextShadowColor) != 0) {
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
start,
|
||||
end,
|
||||
new ShadowStyleSpan(
|
||||
textShadowNode.mTextShadowOffsetDx,
|
||||
textShadowNode.mTextShadowOffsetDy,
|
||||
textShadowNode.mTextShadowRadius,
|
||||
textShadowNode.mTextShadowColor)));
|
||||
}
|
||||
float effectiveLineHeight = textAttributes.getEffectiveLineHeight();
|
||||
if (!Float.isNaN(effectiveLineHeight)
|
||||
&& (parentTextAttributes == null
|
||||
|| parentTextAttributes.getEffectiveLineHeight() != effectiveLineHeight)) {
|
||||
ops.add(new SetSpanOperation(start, end, new CustomLineHeightSpan(effectiveLineHeight)));
|
||||
}
|
||||
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
|
||||
}
|
||||
}
|
||||
|
||||
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
|
||||
protected Spannable spannedFromShadowNode(
|
||||
ReactBaseTextShadowNode textShadowNode,
|
||||
@Nullable String text,
|
||||
boolean supportsInlineViews,
|
||||
@Nullable NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
|
||||
Assertions.assertCondition(
|
||||
!supportsInlineViews || nativeViewHierarchyOptimizer != null,
|
||||
"nativeViewHierarchyOptimizer is required when inline views are supported");
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
|
||||
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
|
||||
|
||||
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
|
||||
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
|
||||
// a new spannable will be wiped out
|
||||
List<SetSpanOperation> ops = new ArrayList<>();
|
||||
Map<Integer, ReactShadowNode> inlineViews = supportsInlineViews ? new HashMap<>() : null;
|
||||
|
||||
if (text != null) {
|
||||
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
|
||||
// TextInput).
|
||||
sb.append(TextTransform.apply(text, textShadowNode.mTextAttributes.textTransform));
|
||||
}
|
||||
|
||||
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0);
|
||||
|
||||
textShadowNode.mContainsImages = false;
|
||||
textShadowNode.mInlineViews = inlineViews;
|
||||
float heightOfTallestInlineViewOrImage = Float.NaN;
|
||||
|
||||
// While setting the Spans on the final text, we also check whether any of them are inline views
|
||||
// or images.
|
||||
for (int priorityIndex = 0; priorityIndex < ops.size(); priorityIndex++) {
|
||||
final SetSpanOperation op = ops.get(ops.size() - priorityIndex - 1);
|
||||
final ReactSpan what = op.what;
|
||||
|
||||
boolean isInlineImage = what instanceof TextInlineImageSpan;
|
||||
if (isInlineImage || what instanceof TextInlineViewPlaceholderSpan) {
|
||||
int height;
|
||||
if (isInlineImage) {
|
||||
height = ((TextInlineImageSpan) what).getHeight();
|
||||
textShadowNode.mContainsImages = true;
|
||||
} else {
|
||||
TextInlineViewPlaceholderSpan placeholder = (TextInlineViewPlaceholderSpan) what;
|
||||
height = placeholder.getHeight();
|
||||
|
||||
// Inline views cannot be layout-only because the ReactTextView needs to be able to grab
|
||||
// ahold of them on the UI thread to size and position them.
|
||||
ReactShadowNode childNode =
|
||||
Assertions.assertNotNull(inlineViews).get(placeholder.getReactTag());
|
||||
|
||||
Assertions.assertNotNull(childNode);
|
||||
Assertions.assertNotNull(nativeViewHierarchyOptimizer);
|
||||
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode);
|
||||
|
||||
// The ReactTextView is responsible for laying out the inline views.
|
||||
childNode.setLayoutParent(textShadowNode);
|
||||
}
|
||||
|
||||
if (Float.isNaN(heightOfTallestInlineViewOrImage)
|
||||
|| height > heightOfTallestInlineViewOrImage) {
|
||||
heightOfTallestInlineViewOrImage = height;
|
||||
}
|
||||
}
|
||||
|
||||
// Actual order of calling {@code execute} does NOT matter,
|
||||
// but the {@code priorityIndex} DOES matter.
|
||||
op.execute(sb, priorityIndex);
|
||||
}
|
||||
|
||||
textShadowNode.mTextAttributes.setHeightOfTallestInlineViewOrImage(
|
||||
heightOfTallestInlineViewOrImage);
|
||||
|
||||
if (mReactTextViewManagerCallback != null) {
|
||||
mReactTextViewManagerCallback.onPostProcessSpannable(sb);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
protected TextAttributes mTextAttributes;
|
||||
|
||||
protected boolean mIsColorSet = false;
|
||||
protected int mColor;
|
||||
protected boolean mIsBackgroundColorSet = false;
|
||||
protected int mBackgroundColor;
|
||||
|
||||
protected @Nullable AccessibilityRole mAccessibilityRole = null;
|
||||
protected @Nullable Role mRole = null;
|
||||
|
||||
protected int mNumberOfLines = ReactConstants.UNSET;
|
||||
protected int mTextAlign = Gravity.NO_GRAVITY;
|
||||
protected int mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
|
||||
protected int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
|
||||
protected int mJustificationMode =
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
|
||||
|
||||
protected float mTextShadowOffsetDx = 0;
|
||||
protected float mTextShadowOffsetDy = 0;
|
||||
protected float mTextShadowRadius = 0;
|
||||
protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
|
||||
|
||||
protected boolean mIsUnderlineTextDecorationSet = false;
|
||||
protected boolean mIsLineThroughTextDecorationSet = false;
|
||||
protected boolean mIncludeFontPadding = true;
|
||||
protected boolean mAdjustsFontSizeToFit = false;
|
||||
protected float mMinimumFontScale = 0;
|
||||
|
||||
/**
|
||||
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link
|
||||
* Typeface#NORMAL} or {@link Typeface#BOLD}.
|
||||
*/
|
||||
protected int mFontStyle = ReactConstants.UNSET;
|
||||
|
||||
protected int mFontWeight = ReactConstants.UNSET;
|
||||
|
||||
/**
|
||||
* NB: If a font family is used that does not have a style in a certain Android version (ie.
|
||||
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
|
||||
* nodes. To retain that style, you have to add it to those nodes explicitly.
|
||||
*
|
||||
* <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;
|
||||
// Only nullable iff `supportsInlineViews` is `false`.
|
||||
protected @Nullable Map<Integer, ReactShadowNode> mInlineViews;
|
||||
|
||||
public ReactBaseTextShadowNode() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ReactBaseTextShadowNode(
|
||||
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
|
||||
mTextAttributes = new TextAttributes();
|
||||
mReactTextViewManagerCallback = reactTextViewManagerCallback;
|
||||
}
|
||||
|
||||
// Return text alignment according to LTR or RTL style
|
||||
private int getTextAlign() {
|
||||
int textAlign = mTextAlign;
|
||||
if (getLayoutDirection() == YogaDirection.RTL) {
|
||||
if (textAlign == Gravity.RIGHT) {
|
||||
textAlign = Gravity.LEFT;
|
||||
} else if (textAlign == Gravity.LEFT) {
|
||||
textAlign = Gravity.RIGHT;
|
||||
}
|
||||
}
|
||||
return textAlign;
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET)
|
||||
public void setNumberOfLines(int numberOfLines) {
|
||||
mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines;
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN)
|
||||
public void setLineHeight(float lineHeight) {
|
||||
mTextAttributes.setLineHeight(lineHeight);
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = 0.f)
|
||||
public void setLetterSpacing(float letterSpacing) {
|
||||
mTextAttributes.setLetterSpacing(letterSpacing);
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
|
||||
public void setAllowFontScaling(boolean allowFontScaling) {
|
||||
if (allowFontScaling != mTextAttributes.getAllowFontScaling()) {
|
||||
mTextAttributes.setAllowFontScaling(allowFontScaling);
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
|
||||
public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
|
||||
if (maxFontSizeMultiplier != mTextAttributes.getMaxFontSizeMultiplier()) {
|
||||
mTextAttributes.setMaxFontSizeMultiplier(maxFontSizeMultiplier);
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_ALIGN)
|
||||
public void setTextAlign(@Nullable String textAlign) {
|
||||
if ("justify".equals(textAlign)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mJustificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD;
|
||||
}
|
||||
mTextAlign = Gravity.LEFT;
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
|
||||
}
|
||||
|
||||
if (textAlign == null || "auto".equals(textAlign)) {
|
||||
mTextAlign = Gravity.NO_GRAVITY;
|
||||
} else if ("left".equals(textAlign)) {
|
||||
mTextAlign = Gravity.LEFT;
|
||||
} else if ("right".equals(textAlign)) {
|
||||
mTextAlign = Gravity.RIGHT;
|
||||
} else if ("center".equals(textAlign)) {
|
||||
mTextAlign = Gravity.CENTER_HORIZONTAL;
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textAlign: " + textAlign);
|
||||
mTextAlign = Gravity.NO_GRAVITY;
|
||||
}
|
||||
}
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
|
||||
public void setFontSize(float fontSize) {
|
||||
mTextAttributes.setFontSize(fontSize);
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.COLOR, customType = "Color")
|
||||
public void setColor(@Nullable Integer color) {
|
||||
if (color != null) {
|
||||
mIsColorSet = true;
|
||||
mColor = color;
|
||||
}
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color")
|
||||
public void setBackgroundColor(@Nullable Integer color) {
|
||||
// Background color needs to be handled here for virtual nodes so it can be incorporated into
|
||||
// the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual
|
||||
// nodes get mapped to native views and native views get their background colors get set via
|
||||
// {@link BaseViewManager}.
|
||||
if (isVirtual()) {
|
||||
if (color != null) {
|
||||
mIsBackgroundColorSet = true;
|
||||
mBackgroundColor = color;
|
||||
}
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
|
||||
public void setAccessibilityRole(@Nullable String accessibilityRole) {
|
||||
if (isVirtual()) {
|
||||
mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole);
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ROLE)
|
||||
public void setRole(@Nullable String role) {
|
||||
if (isVirtual()) {
|
||||
mRole = Role.fromValue(role);
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_FAMILY)
|
||||
public void setFontFamily(@Nullable String fontFamily) {
|
||||
mFontFamily = fontFamily;
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_WEIGHT)
|
||||
public void setFontWeight(@Nullable String fontWeightString) {
|
||||
int fontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString);
|
||||
if (fontWeight != mFontWeight) {
|
||||
mFontWeight = fontWeight;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_VARIANT)
|
||||
public void setFontVariant(@Nullable ReadableArray fontVariantArray) {
|
||||
String fontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariantArray);
|
||||
|
||||
if (!TextUtils.equals(fontFeatureSettings, mFontFeatureSettings)) {
|
||||
mFontFeatureSettings = fontFeatureSettings;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_STYLE)
|
||||
public void setFontStyle(@Nullable String fontStyleString) {
|
||||
int fontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString);
|
||||
if (fontStyle != mFontStyle) {
|
||||
mFontStyle = fontStyle;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
|
||||
public void setIncludeFontPadding(boolean includepad) {
|
||||
mIncludeFontPadding = includepad;
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
|
||||
public void setTextDecorationLine(@Nullable String textDecorationLineString) {
|
||||
mIsUnderlineTextDecorationSet = false;
|
||||
mIsLineThroughTextDecorationSet = false;
|
||||
if (textDecorationLineString != null) {
|
||||
for (String textDecorationLineSubString : textDecorationLineString.split(" ")) {
|
||||
if ("underline".equals(textDecorationLineSubString)) {
|
||||
mIsUnderlineTextDecorationSet = true;
|
||||
} else if ("line-through".equals(textDecorationLineSubString)) {
|
||||
mIsLineThroughTextDecorationSet = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
|
||||
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
|
||||
if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) {
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
|
||||
} else if ("simple".equals(textBreakStrategy)) {
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
|
||||
} else if ("balanced".equals(textBreakStrategy)) {
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: " + textBreakStrategy);
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
|
||||
}
|
||||
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_OFFSET)
|
||||
public void setTextShadowOffset(ReadableMap offsetMap) {
|
||||
mTextShadowOffsetDx = 0;
|
||||
mTextShadowOffsetDy = 0;
|
||||
|
||||
if (offsetMap != null) {
|
||||
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH)
|
||||
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
|
||||
mTextShadowOffsetDx =
|
||||
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH));
|
||||
}
|
||||
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT)
|
||||
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
|
||||
mTextShadowOffsetDy =
|
||||
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT));
|
||||
}
|
||||
}
|
||||
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1)
|
||||
public void setTextShadowRadius(float textShadowRadius) {
|
||||
if (textShadowRadius != mTextShadowRadius) {
|
||||
mTextShadowRadius = textShadowRadius;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
|
||||
public void setTextShadowColor(int textShadowColor) {
|
||||
if (textShadowColor != mTextShadowColor) {
|
||||
mTextShadowColor = textShadowColor;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRANSFORM)
|
||||
public void setTextTransform(@Nullable String textTransform) {
|
||||
TextTransform textTransformEnum = TextTransform.UNSET;
|
||||
if (textTransform == null) {
|
||||
textTransformEnum = TextTransform.UNSET;
|
||||
} else if ("none".equals(textTransform)) {
|
||||
textTransformEnum = TextTransform.NONE;
|
||||
} else if ("uppercase".equals(textTransform)) {
|
||||
textTransformEnum = TextTransform.UPPERCASE;
|
||||
} else if ("lowercase".equals(textTransform)) {
|
||||
textTransformEnum = TextTransform.LOWERCASE;
|
||||
} else if ("capitalize".equals(textTransform)) {
|
||||
textTransformEnum = TextTransform.CAPITALIZE;
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textTransform: " + textTransform);
|
||||
}
|
||||
mTextAttributes.textTransform = textTransformEnum;
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
|
||||
public void setAdjustFontSizeToFit(boolean adjustsFontSizeToFit) {
|
||||
if (adjustsFontSizeToFit != mAdjustsFontSizeToFit) {
|
||||
mAdjustsFontSizeToFit = adjustsFontSizeToFit;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.MINIMUM_FONT_SCALE)
|
||||
public void setMinimumFontScale(float minimumFontScale) {
|
||||
if (minimumFontScale != mMinimumFontScale) {
|
||||
mMinimumFontScale = minimumFontScale;
|
||||
markUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
+647
@@ -0,0 +1,647 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.text
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.text.Layout
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.Gravity
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException
|
||||
import com.facebook.react.uimanager.LayoutShadowNode
|
||||
import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate
|
||||
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole
|
||||
import com.facebook.react.uimanager.ReactShadowNode
|
||||
import com.facebook.react.uimanager.ReactShadowNodeImpl
|
||||
import com.facebook.react.uimanager.ViewProps
|
||||
import com.facebook.react.uimanager.annotations.ReactProp
|
||||
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
|
||||
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant
|
||||
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
||||
import com.facebook.react.views.text.TextTransform.Companion.apply
|
||||
import com.facebook.react.views.text.internal.ReactTextInlineImageShadowNode
|
||||
import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan
|
||||
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan
|
||||
import com.facebook.react.views.text.internal.span.CustomStyleSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactClickableSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactTagSpan
|
||||
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan
|
||||
import com.facebook.react.views.text.internal.span.SetSpanOperation
|
||||
import com.facebook.react.views.text.internal.span.ShadowStyleSpan
|
||||
import com.facebook.react.views.text.internal.span.TextInlineImageSpan
|
||||
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan
|
||||
import com.facebook.yoga.YogaDirection
|
||||
import com.facebook.yoga.YogaUnit
|
||||
|
||||
/**
|
||||
* [ReactShadowNode] abstract class for spannable text nodes.
|
||||
*
|
||||
* This class handles all text attributes associated with `<Text>`-ish node. A concrete node can be
|
||||
* an anchor `<Text>` node, an anchor `<TextInput>` node or virtual `<Text>` node inside `<Text>` or
|
||||
* `<TextInput>` node. Or even something else.
|
||||
*
|
||||
* This also node calculates [Spannable] object based on sub nodes of the same type, which can be
|
||||
* used in concrete classes to feed native views and compute layout.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
public abstract class ReactBaseTextShadowNode
|
||||
@JvmOverloads
|
||||
public constructor(
|
||||
protected var reactTextViewManagerCallback: ReactTextViewManagerCallback? = null
|
||||
) : LayoutShadowNode() {
|
||||
|
||||
protected var textAttributes: TextAttributes = TextAttributes()
|
||||
protected var isColorSet: Boolean = false
|
||||
protected var color: Int = 0
|
||||
protected var isBackgroundColorSet: Boolean = false
|
||||
protected var backgroundColor: Int = 0
|
||||
protected var accessibilityRole: AccessibilityRole? = null
|
||||
protected var role: ReactAccessibilityDelegate.Role? = null
|
||||
protected var numberOfLines: Int = ReactConstants.UNSET
|
||||
private set
|
||||
|
||||
protected var textBreakStrategy: Int = Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
protected var hyphenationFrequency: Int = Layout.HYPHENATION_FREQUENCY_NONE
|
||||
protected var justificationMode: Int =
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
0
|
||||
} else {
|
||||
Layout.JUSTIFICATION_MODE_NONE
|
||||
}
|
||||
|
||||
// Return text alignment according to LTR or RTL style
|
||||
protected var textAlign: Int = Gravity.NO_GRAVITY
|
||||
get() {
|
||||
return if (layoutDirection == YogaDirection.RTL) {
|
||||
when (field) {
|
||||
Gravity.RIGHT -> Gravity.LEFT
|
||||
Gravity.LEFT -> Gravity.RIGHT
|
||||
else -> field
|
||||
}
|
||||
} else {
|
||||
field
|
||||
}
|
||||
}
|
||||
private set
|
||||
|
||||
/**
|
||||
* [fontStyle] can be [Typeface.NORMAL] or [Typeface.ITALIC]. [fontWeight] can be
|
||||
* [Typeface.NORMAL] or [Typeface.BOLD].
|
||||
*/
|
||||
protected var fontStyle: Int = ReactConstants.UNSET
|
||||
|
||||
protected var fontWeight: Int = ReactConstants.UNSET
|
||||
|
||||
/**
|
||||
* NB: If a font family is used that does not have a style in a certain Android version (ie.
|
||||
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
|
||||
* nodes. To retain that style, you have to add it to those nodes explicitly.
|
||||
*
|
||||
* Example, Android 4.4:
|
||||
* <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 var fontFamily: String? = null
|
||||
private set
|
||||
|
||||
/** @see [android.graphics.Paint.setFontFeatureSettings] */
|
||||
protected var fontFeatureSettings: String? = null
|
||||
|
||||
protected var includeFontPadding: Boolean = true
|
||||
private set
|
||||
|
||||
protected var adjustsFontSizeToFit: Boolean = false
|
||||
protected var minimumFontScale: Float = 0f
|
||||
private set
|
||||
|
||||
protected var textShadowOffsetDx: Float = 0f
|
||||
protected var textShadowOffsetDy: Float = 0f
|
||||
protected var textShadowRadius: Float = 0f
|
||||
private set
|
||||
|
||||
protected var textShadowColor: Int = DEFAULT_TEXT_SHADOW_COLOR
|
||||
private set
|
||||
|
||||
protected var isUnderlineTextDecorationSet: Boolean = false
|
||||
protected var isLineThroughTextDecorationSet: Boolean = false
|
||||
protected var containsImages: Boolean = false
|
||||
|
||||
// Only nullable if `supportsInlineViews` is `false`.
|
||||
protected var inlineViews: Map<Int, ReactShadowNode<*>>? = null
|
||||
|
||||
// `nativeViewHierarchyOptimizer` can be `null` as long as `supportsInlineViews` is `false`.
|
||||
@Suppress("DEPRECATION")
|
||||
protected fun spannedFromShadowNode(
|
||||
textShadowNode: ReactBaseTextShadowNode,
|
||||
text: String?,
|
||||
supportsInlineViews: Boolean,
|
||||
nativeViewHierarchyOptimizer: com.facebook.react.uimanager.NativeViewHierarchyOptimizer?
|
||||
): Spannable {
|
||||
check(!supportsInlineViews || nativeViewHierarchyOptimizer != null) {
|
||||
"nativeViewHierarchyOptimizer is required when inline views are supported"
|
||||
}
|
||||
val sb = SpannableStringBuilder()
|
||||
|
||||
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
|
||||
|
||||
// The [SpannableStringBuilder] implementation require setSpan operation to be called
|
||||
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
|
||||
// a new spannable will be wiped out
|
||||
val ops: MutableList<SetSpanOperation> = ArrayList()
|
||||
val inlineViews: MutableMap<Int, ReactShadowNode<*>>? =
|
||||
if (supportsInlineViews) HashMap() else null
|
||||
|
||||
if (text != null) {
|
||||
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
|
||||
// TextInput).
|
||||
sb.append(apply(text, textShadowNode.textAttributes.textTransform))
|
||||
}
|
||||
|
||||
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0)
|
||||
|
||||
textShadowNode.containsImages = false
|
||||
textShadowNode.inlineViews = inlineViews
|
||||
var heightOfTallestInlineViewOrImage = Float.NaN
|
||||
|
||||
// While setting the Spans on the final text, we also check whether any of them are inline views
|
||||
// or images.
|
||||
for (priorityIndex in ops.indices) {
|
||||
val op = ops[ops.size - priorityIndex - 1]
|
||||
val what = op.what
|
||||
|
||||
val isInlineImage = what is TextInlineImageSpan
|
||||
if (isInlineImage || what is TextInlineViewPlaceholderSpan) {
|
||||
val height: Int
|
||||
if (isInlineImage) {
|
||||
height = what.height
|
||||
textShadowNode.containsImages = true
|
||||
} else {
|
||||
val placeholder = what as TextInlineViewPlaceholderSpan
|
||||
height = placeholder.height
|
||||
|
||||
// Inline views cannot be layout-only because ReactTextView needs to access
|
||||
// them on the UI thread to measure and position them.
|
||||
val childNode = checkNotNull(inlineViews)[placeholder.reactTag]
|
||||
|
||||
checkNotNull(childNode)
|
||||
checkNotNull(nativeViewHierarchyOptimizer)
|
||||
nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode)
|
||||
|
||||
// The ReactTextView is responsible for laying out the inline views.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(childNode as ReactShadowNode<ReactShadowNodeImpl>).setLayoutParent(textShadowNode)
|
||||
}
|
||||
|
||||
if (heightOfTallestInlineViewOrImage.isNaN() || height > heightOfTallestInlineViewOrImage) {
|
||||
heightOfTallestInlineViewOrImage = height.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
// Actual order of calling `execute` does NOT matter,
|
||||
// but the `priorityIndex` DOES matter.
|
||||
op.execute(sb, priorityIndex)
|
||||
}
|
||||
|
||||
textShadowNode.textAttributes.heightOfTallestInlineViewOrImage =
|
||||
heightOfTallestInlineViewOrImage
|
||||
|
||||
reactTextViewManagerCallback?.onPostProcessSpannable(sb)
|
||||
|
||||
return sb
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ReactConstants.UNSET)
|
||||
public fun setNumberOfLines(numberOfLines: Int) {
|
||||
this.numberOfLines = if (numberOfLines == 0) ReactConstants.UNSET else numberOfLines
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN)
|
||||
public fun setLineHeight(lineHeight: Float) {
|
||||
textAttributes.lineHeight = lineHeight
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = 0f)
|
||||
public fun setLetterSpacing(letterSpacing: Float) {
|
||||
textAttributes.letterSpacing = letterSpacing
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
|
||||
public fun setAllowFontScaling(allowFontScaling: Boolean) {
|
||||
if (allowFontScaling != textAttributes.allowFontScaling) {
|
||||
textAttributes.allowFontScaling = allowFontScaling
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.MAX_FONT_SIZE_MULTIPLIER, defaultFloat = Float.NaN)
|
||||
public fun setMaxFontSizeMultiplier(maxFontSizeMultiplier: Float) {
|
||||
if (maxFontSizeMultiplier != textAttributes.maxFontSizeMultiplier) {
|
||||
textAttributes.maxFontSizeMultiplier = maxFontSizeMultiplier
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_ALIGN)
|
||||
public fun setTextAlign(textAlign: String?) {
|
||||
if ("justify" == textAlign) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD
|
||||
}
|
||||
this.textAlign = Gravity.LEFT
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
justificationMode = Layout.JUSTIFICATION_MODE_NONE
|
||||
}
|
||||
|
||||
this.textAlign =
|
||||
when (textAlign) {
|
||||
null,
|
||||
"auto" -> Gravity.NO_GRAVITY
|
||||
"left" -> Gravity.LEFT
|
||||
"right" -> Gravity.RIGHT
|
||||
"center" -> Gravity.CENTER_HORIZONTAL
|
||||
else -> {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textAlign: $textAlign")
|
||||
Gravity.NO_GRAVITY
|
||||
}
|
||||
}
|
||||
}
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
|
||||
public fun setFontSize(fontSize: Float) {
|
||||
textAttributes.fontSize = fontSize
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.COLOR, customType = "Color")
|
||||
public fun setColor(color: Int?) {
|
||||
color?.let { newColor ->
|
||||
isColorSet = true
|
||||
this.color = newColor
|
||||
}
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color")
|
||||
public fun setBackgroundColor(color: Int?) {
|
||||
// Background color needs to be handled here for virtual nodes so it can be incorporated into
|
||||
// the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual
|
||||
// nodes get mapped to native views and native views get their background colors get set via
|
||||
// [BaseViewManager].
|
||||
if (isVirtual) {
|
||||
color?.let { newColor ->
|
||||
isBackgroundColorSet = true
|
||||
backgroundColor = newColor
|
||||
}
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
|
||||
public fun setAccessibilityRole(accessibilityRole: String?) {
|
||||
if (isVirtual) {
|
||||
this.accessibilityRole = AccessibilityRole.fromValue(accessibilityRole)
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ROLE)
|
||||
public fun setRole(role: String?) {
|
||||
if (isVirtual) {
|
||||
this.role = ReactAccessibilityDelegate.Role.fromValue(role)
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_FAMILY)
|
||||
public fun setFontFamily(fontFamily: String?) {
|
||||
this.fontFamily = fontFamily
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_WEIGHT)
|
||||
public fun setFontWeight(fontWeightString: String?) {
|
||||
val parsedFontWeight = parseFontWeight(fontWeightString)
|
||||
if (parsedFontWeight != fontWeight) {
|
||||
fontWeight = parsedFontWeight
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_VARIANT)
|
||||
public fun setFontVariant(fontVariantArray: ReadableArray?) {
|
||||
val fontFeatureSettings = parseFontVariant(fontVariantArray)
|
||||
|
||||
if (fontFeatureSettings != this.fontFeatureSettings) {
|
||||
this.fontFeatureSettings = fontFeatureSettings
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.FONT_STYLE)
|
||||
public fun setFontStyle(fontStyleString: String?) {
|
||||
val parsedFontStyle = parseFontStyle(fontStyleString)
|
||||
if (parsedFontStyle != fontStyle) {
|
||||
fontStyle = parsedFontStyle
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
|
||||
public fun setIncludeFontPadding(includepad: Boolean) {
|
||||
includeFontPadding = includepad
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
|
||||
public fun setTextDecorationLine(textDecorationLineString: String?) {
|
||||
isUnderlineTextDecorationSet = false
|
||||
isLineThroughTextDecorationSet = false
|
||||
if (textDecorationLineString != null) {
|
||||
for (textDecorationLineSubString in
|
||||
textDecorationLineString.split(" ").dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
if ("underline" == textDecorationLineSubString) {
|
||||
isUnderlineTextDecorationSet = true
|
||||
} else if ("line-through" == textDecorationLineSubString) {
|
||||
isLineThroughTextDecorationSet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
|
||||
public open fun setTextBreakStrategy(textBreakStrategy: String?) {
|
||||
this.textBreakStrategy =
|
||||
when (textBreakStrategy) {
|
||||
null,
|
||||
"highQuality" -> Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
"simple" -> Layout.BREAK_STRATEGY_SIMPLE
|
||||
"balanced" -> Layout.BREAK_STRATEGY_BALANCED
|
||||
else -> {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: $textBreakStrategy")
|
||||
Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
}
|
||||
}
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_OFFSET)
|
||||
public fun setTextShadowOffset(offsetMap: ReadableMap?) {
|
||||
textShadowOffsetDx = 0f
|
||||
textShadowOffsetDy = 0f
|
||||
|
||||
offsetMap?.let { map ->
|
||||
if (map.hasKey(PROP_SHADOW_OFFSET_WIDTH) && !map.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
|
||||
textShadowOffsetDx = toPixelFromDIP(map.getDouble(PROP_SHADOW_OFFSET_WIDTH))
|
||||
}
|
||||
if (map.hasKey(PROP_SHADOW_OFFSET_HEIGHT) && !map.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
|
||||
textShadowOffsetDy = toPixelFromDIP(map.getDouble(PROP_SHADOW_OFFSET_HEIGHT))
|
||||
}
|
||||
}
|
||||
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_RADIUS, defaultInt = 1)
|
||||
public fun setTextShadowRadius(textShadowRadius: Float) {
|
||||
if (textShadowRadius != this.textShadowRadius) {
|
||||
this.textShadowRadius = textShadowRadius
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
|
||||
public fun setTextShadowColor(textShadowColor: Int) {
|
||||
if (textShadowColor != this.textShadowColor) {
|
||||
this.textShadowColor = textShadowColor
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRANSFORM)
|
||||
public fun setTextTransform(textTransform: String?) {
|
||||
val textTransformEnum =
|
||||
when (textTransform) {
|
||||
null -> TextTransform.UNSET
|
||||
"none" -> TextTransform.NONE
|
||||
"uppercase" -> TextTransform.UPPERCASE
|
||||
"lowercase" -> TextTransform.LOWERCASE
|
||||
"capitalize" -> TextTransform.CAPITALIZE
|
||||
else -> {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textTransform: $textTransform")
|
||||
TextTransform.UNSET
|
||||
}
|
||||
}
|
||||
textAttributes.textTransform = textTransformEnum
|
||||
markUpdated()
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.ADJUSTS_FONT_SIZE_TO_FIT)
|
||||
public fun setAdjustFontSizeToFit(adjustsFontSizeToFit: Boolean) {
|
||||
if (adjustsFontSizeToFit != this.adjustsFontSizeToFit) {
|
||||
this.adjustsFontSizeToFit = adjustsFontSizeToFit
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.MINIMUM_FONT_SCALE)
|
||||
public fun setMinimumFontScale(minimumFontScale: Float) {
|
||||
if (minimumFontScale != this.minimumFontScale) {
|
||||
this.minimumFontScale = minimumFontScale
|
||||
markUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
// Use a direction weak character so the placeholder doesn't change the direction of the
|
||||
// previous character. https://en.wikipedia.org/wiki/Bi-directional_text#weak_characters
|
||||
private const val INLINE_VIEW_PLACEHOLDER = "0"
|
||||
|
||||
public const val PROP_SHADOW_OFFSET: String = "textShadowOffset"
|
||||
public const val PROP_SHADOW_OFFSET_WIDTH: String = "width"
|
||||
public const val PROP_SHADOW_OFFSET_HEIGHT: String = "height"
|
||||
public const val PROP_SHADOW_RADIUS: String = "textShadowRadius"
|
||||
public const val PROP_SHADOW_COLOR: String = "textShadowColor"
|
||||
|
||||
public const val PROP_TEXT_TRANSFORM: String = "textTransform"
|
||||
|
||||
public const val DEFAULT_TEXT_SHADOW_COLOR: Int = 0x55000000
|
||||
|
||||
private fun buildSpannedFromShadowNode(
|
||||
textShadowNode: ReactBaseTextShadowNode,
|
||||
sb: SpannableStringBuilder,
|
||||
ops: MutableList<SetSpanOperation>,
|
||||
parentTextAttributes: TextAttributes?,
|
||||
supportsInlineViews: Boolean,
|
||||
inlineViews: MutableMap<Int, ReactShadowNode<*>>?,
|
||||
start: Int
|
||||
) {
|
||||
val textAttributes =
|
||||
parentTextAttributes?.applyChild(textShadowNode.textAttributes)
|
||||
?: textShadowNode.textAttributes
|
||||
|
||||
var i = 0
|
||||
val length = textShadowNode.childCount
|
||||
while (i < length) {
|
||||
val child: ReactShadowNode<*> = textShadowNode.getChildAt(i)
|
||||
|
||||
if (child is ReactRawTextShadowNode) {
|
||||
child.text?.let { sb.append(apply(it, textAttributes.textTransform)) }
|
||||
} else if (child is ReactBaseTextShadowNode) {
|
||||
buildSpannedFromShadowNode(
|
||||
child, sb, ops, textAttributes, supportsInlineViews, inlineViews, sb.length)
|
||||
} else if (child is ReactTextInlineImageShadowNode) {
|
||||
// We make the image take up 1 character in the span and put a corresponding character
|
||||
// into the text so that the image doesn't run over any following text.
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER)
|
||||
ops.add(
|
||||
SetSpanOperation(
|
||||
sb.length - INLINE_VIEW_PLACEHOLDER.length,
|
||||
sb.length,
|
||||
child.buildInlineImageSpan()))
|
||||
} else if (supportsInlineViews) {
|
||||
val reactTag = child.reactTag
|
||||
val widthValue = child.styleWidth
|
||||
val heightValue = child.styleHeight
|
||||
|
||||
val width: Float
|
||||
val height: Float
|
||||
if (widthValue.unit != YogaUnit.POINT || heightValue.unit != YogaUnit.POINT) {
|
||||
// If the measurement of the child isn't calculated, we calculate the layout for the
|
||||
// view using Yoga
|
||||
child.calculateLayout()
|
||||
width = child.layoutWidth
|
||||
height = child.layoutHeight
|
||||
} else {
|
||||
width = widthValue.value
|
||||
height = heightValue.value
|
||||
}
|
||||
|
||||
// We make the inline view take up 1 character in the span and put a corresponding
|
||||
// character
|
||||
// into
|
||||
// the text so that the inline view doesn't run over any following text.
|
||||
sb.append(INLINE_VIEW_PLACEHOLDER)
|
||||
ops.add(
|
||||
SetSpanOperation(
|
||||
sb.length - INLINE_VIEW_PLACEHOLDER.length,
|
||||
sb.length,
|
||||
TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt())))
|
||||
|
||||
// supportsInlineViews is true, so we can assume that inlineViews is not null
|
||||
checkNotNull(inlineViews)[reactTag] = child
|
||||
} else {
|
||||
throw IllegalViewOperationException(
|
||||
"Unexpected view type nested under a <Text> or <TextInput> node: ${child.javaClass}")
|
||||
}
|
||||
child.markUpdateSeen()
|
||||
i++
|
||||
}
|
||||
val end = sb.length
|
||||
if (end >= start) {
|
||||
if (textShadowNode.isColorSet) {
|
||||
ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textShadowNode.color)))
|
||||
}
|
||||
if (textShadowNode.isBackgroundColorSet) {
|
||||
ops.add(
|
||||
SetSpanOperation(
|
||||
start, end, ReactBackgroundColorSpan(textShadowNode.backgroundColor)))
|
||||
}
|
||||
val roleIsLink =
|
||||
if (textShadowNode.role != null)
|
||||
textShadowNode.role == ReactAccessibilityDelegate.Role.LINK
|
||||
else textShadowNode.accessibilityRole == AccessibilityRole.LINK
|
||||
if (roleIsLink) {
|
||||
ops.add(SetSpanOperation(start, end, ReactClickableSpan(textShadowNode.reactTag)))
|
||||
}
|
||||
val effectiveLetterSpacing = textAttributes.effectiveLetterSpacing
|
||||
if (!java.lang.Float.isNaN(effectiveLetterSpacing) &&
|
||||
(parentTextAttributes == null ||
|
||||
parentTextAttributes.effectiveLetterSpacing != effectiveLetterSpacing)) {
|
||||
ops.add(SetSpanOperation(start, end, CustomLetterSpacingSpan(effectiveLetterSpacing)))
|
||||
}
|
||||
val effectiveFontSize = textAttributes.effectiveFontSize
|
||||
if ( // `getEffectiveFontSize` always returns a value so don't need to check for anything
|
||||
// like `Float.NaN`.
|
||||
parentTextAttributes == null ||
|
||||
parentTextAttributes.effectiveFontSize != effectiveFontSize) {
|
||||
ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(effectiveFontSize)))
|
||||
}
|
||||
if (textShadowNode.fontStyle != ReactConstants.UNSET ||
|
||||
textShadowNode.fontWeight != ReactConstants.UNSET ||
|
||||
textShadowNode.fontFamily != null) {
|
||||
ops.add(
|
||||
SetSpanOperation(
|
||||
start,
|
||||
end,
|
||||
CustomStyleSpan(
|
||||
textShadowNode.fontStyle,
|
||||
textShadowNode.fontWeight,
|
||||
textShadowNode.fontFeatureSettings,
|
||||
textShadowNode.fontFamily,
|
||||
textShadowNode.themedContext.assets)))
|
||||
}
|
||||
if (textShadowNode.isUnderlineTextDecorationSet) {
|
||||
ops.add(SetSpanOperation(start, end, ReactUnderlineSpan()))
|
||||
}
|
||||
if (textShadowNode.isLineThroughTextDecorationSet) {
|
||||
ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan()))
|
||||
}
|
||||
if ((textShadowNode.textShadowOffsetDx != 0f ||
|
||||
textShadowNode.textShadowOffsetDy != 0f ||
|
||||
textShadowNode.textShadowRadius != 0f) &&
|
||||
Color.alpha(textShadowNode.textShadowColor) != 0) {
|
||||
ops.add(
|
||||
SetSpanOperation(
|
||||
start,
|
||||
end,
|
||||
ShadowStyleSpan(
|
||||
textShadowNode.textShadowOffsetDx,
|
||||
textShadowNode.textShadowOffsetDy,
|
||||
textShadowNode.textShadowRadius,
|
||||
textShadowNode.textShadowColor)))
|
||||
}
|
||||
val effectiveLineHeight = textAttributes.effectiveLineHeight
|
||||
if (!java.lang.Float.isNaN(effectiveLineHeight) &&
|
||||
(parentTextAttributes == null ||
|
||||
parentTextAttributes.effectiveLineHeight != effectiveLineHeight)) {
|
||||
ops.add(SetSpanOperation(start, end, CustomLineHeightSpan(effectiveLineHeight)))
|
||||
}
|
||||
ops.add(SetSpanOperation(start, end, ReactTagSpan(textShadowNode.reactTag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-27
@@ -66,15 +66,15 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
}
|
||||
var layout = measureSpannedText(text, width, widthMode)
|
||||
|
||||
if (mAdjustsFontSizeToFit) {
|
||||
val initialFontSize = mTextAttributes.effectiveFontSize
|
||||
var currentFontSize = mTextAttributes.effectiveFontSize
|
||||
if (adjustsFontSizeToFit) {
|
||||
val initialFontSize = textAttributes.effectiveFontSize
|
||||
var currentFontSize = textAttributes.effectiveFontSize
|
||||
// Minimum font size is 4pts to match the iOS implementation.
|
||||
val minimumFontSize =
|
||||
max((mMinimumFontScale * initialFontSize), PixelUtil.toPixelFromDIP(4f)).toInt()
|
||||
max((minimumFontScale * initialFontSize), PixelUtil.toPixelFromDIP(4f)).toInt()
|
||||
while (currentFontSize > minimumFontSize &&
|
||||
(mNumberOfLines != ReactConstants.UNSET && layout.lineCount > mNumberOfLines ||
|
||||
heightMode != YogaMeasureMode.UNDEFINED && layout.height > height)) {
|
||||
((numberOfLines != ReactConstants.UNSET && layout.lineCount > numberOfLines) ||
|
||||
(heightMode != YogaMeasureMode.UNDEFINED && layout.height > height))) {
|
||||
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
|
||||
// measurements
|
||||
// based on the number of points the font size needs to be reduced by.
|
||||
@@ -111,8 +111,11 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
}
|
||||
|
||||
val lineCount =
|
||||
if (mNumberOfLines == ReactConstants.UNSET) layout.lineCount
|
||||
else min(mNumberOfLines.toDouble(), layout.lineCount.toDouble()).toInt()
|
||||
if (numberOfLines == ReactConstants.UNSET) {
|
||||
layout.lineCount
|
||||
} else {
|
||||
min(numberOfLines.toDouble(), layout.lineCount.toDouble()).toInt()
|
||||
}
|
||||
|
||||
// Instead of using `layout.getWidth()` (which may yield a significantly larger width
|
||||
// for
|
||||
@@ -175,7 +178,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
|
||||
var width = width
|
||||
val textPaint = textPaintInstance
|
||||
textPaint.textSize = mTextAttributes.effectiveFontSize.toFloat()
|
||||
textPaint.textSize = textAttributes.effectiveFontSize.toFloat()
|
||||
val layout: Layout
|
||||
val boring = BoringLayout.isBoring(text, textPaint)
|
||||
val desiredWidth = if (boring == null) Layout.getDesiredWidth(text, textPaint) else Float.NaN
|
||||
@@ -184,7 +187,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
val unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0
|
||||
|
||||
val alignment =
|
||||
when (textAlign) {
|
||||
when (_textAlign) {
|
||||
Gravity.LEFT -> Layout.Alignment.ALIGN_NORMAL
|
||||
Gravity.RIGHT -> Layout.Alignment.ALIGN_OPPOSITE
|
||||
Gravity.CENTER_HORIZONTAL -> Layout.Alignment.ALIGN_CENTER
|
||||
@@ -201,12 +204,12 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, hintWidth)
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0f, 1f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency)
|
||||
.setIncludePad(includeFontPadding)
|
||||
.setBreakStrategy(textBreakStrategy)
|
||||
.setHyphenationFrequency(hyphenationFrequency)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setJustificationMode(mJustificationMode)
|
||||
builder.setJustificationMode(justificationMode)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
builder.setUseLineSpacingFromFallbacks(true)
|
||||
@@ -224,7 +227,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
1f,
|
||||
0f,
|
||||
boring,
|
||||
mIncludeFontPadding)
|
||||
includeFontPadding)
|
||||
} else {
|
||||
// Is used for multiline, boring text and the width is known.
|
||||
// Android 11+ introduces changes in text width calculation which leads to cases
|
||||
@@ -238,12 +241,12 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, width.toInt())
|
||||
.setAlignment(alignment)
|
||||
.setLineSpacing(0f, 1f)
|
||||
.setIncludePad(mIncludeFontPadding)
|
||||
.setBreakStrategy(mTextBreakStrategy)
|
||||
.setHyphenationFrequency(mHyphenationFrequency)
|
||||
.setIncludePad(includeFontPadding)
|
||||
.setBreakStrategy(textBreakStrategy)
|
||||
.setHyphenationFrequency(hyphenationFrequency)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setJustificationMode(mJustificationMode)
|
||||
builder.setJustificationMode(justificationMode)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -255,9 +258,9 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
}
|
||||
|
||||
// Return text alignment according to LTR or RTL style
|
||||
private val textAlign: Int
|
||||
private val _textAlign: Int
|
||||
get() {
|
||||
var textAlign = mTextAlign
|
||||
var textAlign = super.textAlign
|
||||
if (layoutDirection == YogaDirection.RTL) {
|
||||
if (textAlign == Gravity.RIGHT) {
|
||||
textAlign = Gravity.LEFT
|
||||
@@ -300,14 +303,14 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
ReactTextUpdate(
|
||||
text,
|
||||
ReactConstants.UNSET,
|
||||
mContainsImages,
|
||||
containsImages,
|
||||
getPadding(Spacing.START),
|
||||
getPadding(Spacing.TOP),
|
||||
getPadding(Spacing.END),
|
||||
getPadding(Spacing.BOTTOM),
|
||||
textAlign,
|
||||
mTextBreakStrategy,
|
||||
mJustificationMode)
|
||||
_textAlign,
|
||||
textBreakStrategy,
|
||||
justificationMode)
|
||||
uiViewOperationQueue.enqueueUpdateExtraData(reactTag, reactTextUpdate)
|
||||
}
|
||||
|
||||
@@ -319,7 +322,9 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
override fun calculateLayoutOnChildren(): Iterable<ReactShadowNode<*>?>? {
|
||||
// Run flexbox on and return the descendants which are inline views.
|
||||
|
||||
if (mInlineViews.isNullOrEmpty()) return null
|
||||
if (inlineViews.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val text: Spanned =
|
||||
checkNotNull(preparedSpannableText) {
|
||||
@@ -329,7 +334,7 @@ public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? =
|
||||
val shadowNodes = mutableListOf<ReactShadowNode<*>?>()
|
||||
|
||||
for (placeholder in placeholders) {
|
||||
val child = mInlineViews?.get(placeholder.reactTag)
|
||||
val child = inlineViews?.get(placeholder.reactTag)
|
||||
checkNotNull(child) { "Child is null" }
|
||||
child.calculateLayout()
|
||||
shadowNodes.add(child)
|
||||
|
||||
+14
-14
@@ -63,7 +63,7 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
|
||||
}
|
||||
|
||||
init {
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
setMeasureFunction(this)
|
||||
}
|
||||
|
||||
@@ -110,15 +110,15 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
|
||||
if (localData != null) {
|
||||
localData?.apply(editText)
|
||||
} else {
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextAttributes.effectiveFontSize.toFloat())
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textAttributes.effectiveFontSize.toFloat())
|
||||
|
||||
if (mNumberOfLines != ReactConstants.UNSET) {
|
||||
editText.setLines(mNumberOfLines)
|
||||
if (numberOfLines != ReactConstants.UNSET) {
|
||||
editText.setLines(numberOfLines)
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
if (editText.breakStrategy != mTextBreakStrategy) {
|
||||
editText.breakStrategy = mTextBreakStrategy
|
||||
if (editText.breakStrategy != textBreakStrategy) {
|
||||
editText.breakStrategy = textBreakStrategy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,12 +152,12 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
|
||||
override fun setTextBreakStrategy(textBreakStrategy: String?) {
|
||||
when (textBreakStrategy) {
|
||||
null,
|
||||
"simple" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
|
||||
"highQuality" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
"balanced" -> mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED
|
||||
"simple" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
|
||||
"highQuality" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY
|
||||
"balanced" -> this.textBreakStrategy = Layout.BREAK_STRATEGY_BALANCED
|
||||
else -> {
|
||||
FLog.w(ReactConstants.TAG, "Invalid textBreakStrategy: $textBreakStrategy")
|
||||
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
|
||||
this.textBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,14 +175,14 @@ constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null)
|
||||
null // only needed to support inline views
|
||||
),
|
||||
mostRecentEventCount,
|
||||
mContainsImages,
|
||||
containsImages,
|
||||
getPadding(Spacing.LEFT),
|
||||
getPadding(Spacing.TOP),
|
||||
getPadding(Spacing.RIGHT),
|
||||
getPadding(Spacing.BOTTOM),
|
||||
mTextAlign,
|
||||
mTextBreakStrategy,
|
||||
mJustificationMode)
|
||||
textAlign,
|
||||
textBreakStrategy,
|
||||
justificationMode)
|
||||
uiViewOperationQueue.enqueueUpdateExtraData(reactTag, reactTextUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user