diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInput.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInput.java
index 2af86a5a734..221117a6c8a 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInput.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTTextInput.java
@@ -9,7 +9,8 @@
package com.facebook.react.flat;
-import javax.annotation.Nullable;
+import static com.facebook.react.views.text.ReactRawTextShadowNode.PROP_TEXT;
+import static com.facebook.react.views.text.ReactTextShadowNode.UNSET;
import android.annotation.TargetApi;
import android.os.Build;
@@ -17,11 +18,6 @@ import android.text.SpannableStringBuilder;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.EditText;
-
-import com.facebook.yoga.YogaMeasureMode;
-import com.facebook.yoga.YogaMeasureFunction;
-import com.facebook.yoga.YogaNode;
-import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.Spacing;
@@ -32,9 +28,11 @@ import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.view.MeasureUtil;
-
-import static com.facebook.react.views.text.ReactTextShadowNode.PROP_TEXT;
-import static com.facebook.react.views.text.ReactTextShadowNode.UNSET;
+import com.facebook.yoga.YogaMeasureFunction;
+import com.facebook.yoga.YogaMeasureMode;
+import com.facebook.yoga.YogaMeasureOutput;
+import com.facebook.yoga.YogaNode;
+import javax.annotation.Nullable;
public class RCTTextInput extends RCTVirtualText implements AndroidView, YogaMeasureFunction {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
new file mode 100644
index 00000000000..7dc50f710c6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
@@ -0,0 +1,504 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.facebook.react.views.text;
+
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.UnderlineSpan;
+import android.view.Gravity;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.uimanager.IllegalViewOperationException;
+import com.facebook.react.uimanager.LayoutShadowNode;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.ReactShadowNode;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.uimanager.annotations.ReactProp;
+import com.facebook.yoga.YogaDirection;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ReactShadowNode} abstract class for spannable text nodes.
+ *
+ *
This class handles all text attributes assosiated with {@code }-ish node. A concrete
+ * node can be an anchor {@code } node, an anchor {@code } node or virtual {@code
+ * } node inside {@code } or {@code } node. Or even something else.
+ *
+ * This also node calculates {@link Spannable} object based on subnodes of the same type, which
+ * can be used in concrete classes to feed native views and compute layout.
+ */
+public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
+
+ private static final String INLINE_IMAGE_PLACEHOLDER = "I";
+ public static final int UNSET = -1;
+
+ 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 int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
+
+ private static class SetSpanOperation {
+ protected int start, end;
+ protected Object what;
+
+ SetSpanOperation(int start, int end, Object what) {
+ this.start = start;
+ this.end = end;
+ this.what = what;
+ }
+
+ public void execute(SpannableStringBuilder sb) {
+ // All spans will automatically extend to the right of the text, but not the left - except
+ // for spans that start at the beginning of the text.
+ int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
+ if (start == 0) {
+ spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
+ }
+ sb.setSpan(what, start, end, spanFlags);
+ }
+ }
+
+ private static void buildSpannedFromShadowNode(
+ ReactBaseTextShadowNode textShadowNode,
+ SpannableStringBuilder sb,
+ List ops) {
+
+ int start = sb.length();
+
+ for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
+ ReactShadowNode child = textShadowNode.getChildAt(i);
+
+ if (child instanceof ReactRawTextShadowNode) {
+ sb.append(((ReactRawTextShadowNode) child).getText());
+ } else if (child instanceof ReactBaseTextShadowNode) {
+ buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops);
+ } 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_IMAGE_PLACEHOLDER);
+ ops.add(
+ new SetSpanOperation(
+ sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
+ sb.length(),
+ ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
+ } else {
+ throw new IllegalViewOperationException(
+ "Unexpected view type nested under text node: " + child.getClass());
+ }
+ child.markUpdateSeen();
+ }
+ int end = sb.length();
+ if (end >= start) {
+ if (textShadowNode.mIsColorSet) {
+ ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textShadowNode.mColor)));
+ }
+ if (textShadowNode.mIsBackgroundColorSet) {
+ ops.add(
+ new SetSpanOperation(
+ start, end, new BackgroundColorSpan(textShadowNode.mBackgroundColor)));
+ }
+ if (textShadowNode.mFontSize != UNSET) {
+ ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textShadowNode.mFontSize)));
+ }
+ if (textShadowNode.mFontStyle != UNSET
+ || textShadowNode.mFontWeight != UNSET
+ || textShadowNode.mFontFamily != null) {
+ ops.add(
+ new SetSpanOperation(
+ start,
+ end,
+ new CustomStyleSpan(
+ textShadowNode.mFontStyle,
+ textShadowNode.mFontWeight,
+ textShadowNode.mFontFamily,
+ textShadowNode.getThemedContext().getAssets())));
+ }
+ if (textShadowNode.mIsUnderlineTextDecorationSet) {
+ ops.add(new SetSpanOperation(start, end, new UnderlineSpan()));
+ }
+ if (textShadowNode.mIsLineThroughTextDecorationSet) {
+ ops.add(new SetSpanOperation(start, end, new StrikethroughSpan()));
+ }
+ if (textShadowNode.mTextShadowOffsetDx != 0 || textShadowNode.mTextShadowOffsetDy != 0) {
+ ops.add(
+ new SetSpanOperation(
+ start,
+ end,
+ new ShadowStyleSpan(
+ textShadowNode.mTextShadowOffsetDx,
+ textShadowNode.mTextShadowOffsetDy,
+ textShadowNode.mTextShadowRadius,
+ textShadowNode.mTextShadowColor)));
+ }
+ if (!Float.isNaN(textShadowNode.getEffectiveLineHeight())) {
+ ops.add(
+ new SetSpanOperation(
+ start, end, new CustomLineHeightSpan(textShadowNode.getEffectiveLineHeight())));
+ }
+ ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
+ }
+ }
+
+ protected static Spannable spannedFromShadowNode(
+ ReactBaseTextShadowNode textShadowNode, String text) {
+ 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 withing the region for which one may set
+ // a new spannable will be wiped out
+ List ops = new ArrayList<>();
+ buildSpannedFromShadowNode(textShadowNode, sb, ops);
+
+ if (text != null) {
+ sb.append(text);
+ }
+
+ if (textShadowNode.mFontSize == UNSET) {
+ sb.setSpan(
+ new AbsoluteSizeSpan(
+ textShadowNode.mAllowFontScaling
+ ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
+ : (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP))),
+ 0,
+ sb.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ textShadowNode.mContainsImages = false;
+ textShadowNode.mHeightOfTallestInlineImage = Float.NaN;
+
+ // While setting the Spans on the final text, we also check whether any of them are images
+ for (int i = ops.size() - 1; i >= 0; i--) {
+ SetSpanOperation op = ops.get(i);
+ if (op.what instanceof TextInlineImageSpan) {
+ int height = ((TextInlineImageSpan) op.what).getHeight();
+ textShadowNode.mContainsImages = true;
+ if (Float.isNaN(textShadowNode.mHeightOfTallestInlineImage)
+ || height > textShadowNode.mHeightOfTallestInlineImage) {
+ textShadowNode.mHeightOfTallestInlineImage = height;
+ }
+ }
+ op.execute(sb);
+ }
+ return sb;
+ }
+
+ /**
+ * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
+ * return the weight.
+ *
+ * This code is duplicated in ReactTextInputManager
+ * TODO: Factor into a common place they can both use
+ */
+ private static int parseNumericFontWeight(String fontWeightString) {
+ // This should be much faster than using regex to verify input and Integer.parseInt
+ return fontWeightString.length() == 3
+ && fontWeightString.endsWith("00")
+ && fontWeightString.charAt(0) <= '9'
+ && fontWeightString.charAt(0) >= '1'
+ ? 100 * (fontWeightString.charAt(0) - '0')
+ : -1;
+ }
+
+ protected float mLineHeight = Float.NaN;
+ protected boolean mIsColorSet = false;
+ protected boolean mAllowFontScaling = true;
+ protected int mColor;
+ protected boolean mIsBackgroundColorSet = false;
+ protected int mBackgroundColor;
+
+ protected int mNumberOfLines = UNSET;
+ protected int mFontSize = UNSET;
+ protected float mFontSizeInput = UNSET;
+ protected float mLineHeightInput = UNSET;
+ protected int mTextAlign = Gravity.NO_GRAVITY;
+ protected int mTextBreakStrategy =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
+
+ protected float mTextShadowOffsetDx = 0;
+ protected float mTextShadowOffsetDy = 0;
+ protected float mTextShadowRadius = 1;
+ protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
+
+ protected boolean mIsUnderlineTextDecorationSet = false;
+ protected boolean mIsLineThroughTextDecorationSet = false;
+ protected boolean mIncludeFontPadding = true;
+
+ /**
+ * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
+ * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
+ */
+ protected int mFontStyle = UNSET;
+
+ protected int mFontWeight = UNSET;
+ /**
+ * NB: If a font family is used that does not have a style in a certain Android version (ie.
+ * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
+ * nodes. To retain that style, you have to add it to those nodes explicitly.
+ * Example, Android 4.4:
+ * Bold Text
+ * Bold Text
+ * Bold Text
+ *
+ * Not Bold Text
+ * Not Bold Text
+ * Not Bold Text
+ *
+ * Not Bold Text
+ * Bold Text
+ * Bold Text
+ */
+ protected @Nullable String mFontFamily = null;
+
+ protected boolean mContainsImages = false;
+ protected float mHeightOfTallestInlineImage = Float.NaN;
+
+ // 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;
+ }
+
+ // 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 = UNSET)
+ public void setNumberOfLines(int numberOfLines) {
+ mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
+ markUpdated();
+ }
+
+ @ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = UNSET)
+ public void setLineHeight(float lineHeight) {
+ mLineHeightInput = lineHeight;
+ if (lineHeight == UNSET) {
+ mLineHeight = Float.NaN;
+ } else {
+ mLineHeight =
+ mAllowFontScaling
+ ? PixelUtil.toPixelFromSP(lineHeight)
+ : PixelUtil.toPixelFromDIP(lineHeight);
+ }
+ markUpdated();
+ }
+
+ @ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
+ public void setAllowFontScaling(boolean allowFontScaling) {
+ if (allowFontScaling != mAllowFontScaling) {
+ mAllowFontScaling = allowFontScaling;
+ setFontSize(mFontSizeInput);
+ setLineHeight(mLineHeightInput);
+ markUpdated();
+ }
+ }
+
+ @ReactProp(name = ViewProps.TEXT_ALIGN)
+ public void setTextAlign(@Nullable String textAlign) {
+ 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 if ("justify".equals(textAlign)) {
+ // Fallback gracefully for cross-platform compat instead of error
+ mTextAlign = Gravity.LEFT;
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
+ }
+ markUpdated();
+ }
+
+ @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = UNSET)
+ public void setFontSize(float fontSize) {
+ mFontSizeInput = fontSize;
+ if (fontSize != UNSET) {
+ fontSize =
+ mAllowFontScaling
+ ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
+ : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
+ }
+ mFontSize = (int) fontSize;
+ markUpdated();
+ }
+
+ @ReactProp(name = ViewProps.COLOR)
+ public void setColor(@Nullable Integer color) {
+ mIsColorSet = (color != null);
+ if (mIsColorSet) {
+ mColor = color;
+ }
+ markUpdated();
+ }
+
+ @ReactProp(name = ViewProps.BACKGROUND_COLOR)
+ public void setBackgroundColor(Integer color) {
+ // Don't apply background color to anchor TextView since it will be applied on the View directly
+ if (!isVirtualAnchor()) {
+ mIsBackgroundColorSet = (color != null);
+ if (mIsBackgroundColorSet) {
+ mBackgroundColor = color;
+ }
+ markUpdated();
+ }
+ }
+
+ @ReactProp(name = ViewProps.FONT_FAMILY)
+ public void setFontFamily(@Nullable String fontFamily) {
+ mFontFamily = fontFamily;
+ markUpdated();
+ }
+
+ /**
+ /* This code is duplicated in ReactTextInputManager
+ /* TODO: Factor into a common place they can both use
+ */
+ @ReactProp(name = ViewProps.FONT_WEIGHT)
+ public void setFontWeight(@Nullable String fontWeightString) {
+ int fontWeightNumeric =
+ fontWeightString != null ? parseNumericFontWeight(fontWeightString) : -1;
+ int fontWeight = UNSET;
+ if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) {
+ fontWeight = Typeface.BOLD;
+ } else if ("normal".equals(fontWeightString)
+ || (fontWeightNumeric != -1 && fontWeightNumeric < 500)) {
+ fontWeight = Typeface.NORMAL;
+ }
+ if (fontWeight != mFontWeight) {
+ mFontWeight = fontWeight;
+ markUpdated();
+ }
+ }
+
+ /**
+ /* This code is duplicated in ReactTextInputManager
+ /* TODO: Factor into a common place they can both use
+ */
+ @ReactProp(name = ViewProps.FONT_STYLE)
+ public void setFontStyle(@Nullable String fontStyleString) {
+ int fontStyle = UNSET;
+ if ("italic".equals(fontStyleString)) {
+ fontStyle = Typeface.ITALIC;
+ } else if ("normal".equals(fontStyleString)) {
+ fontStyle = Typeface.NORMAL;
+ }
+ 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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return;
+ }
+
+ 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 {
+ throw new JSApplicationIllegalArgumentException(
+ "Invalid textBreakStrategy: " + textBreakStrategy);
+ }
+
+ 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();
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java
new file mode 100644
index 00000000000..22eba4e915a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the LICENSE file in the root
+ * directory of this source tree. An additional grant of patent rights can be found in the PATENTS
+ * file in the same directory.
+ */
+
+package com.facebook.react.views.text;
+
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.uimanager.BaseViewManager;
+import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.Spacing;
+import com.facebook.react.uimanager.ViewDefaults;
+import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.uimanager.annotations.ReactProp;
+import com.facebook.react.uimanager.annotations.ReactPropGroup;
+import com.facebook.yoga.YogaConstants;
+import javax.annotation.Nullable;
+
+/**
+ * Abstract class for anchor {@code }-ish spannable views, such as {@link TextView} or {@link
+ * TextEdit}.
+ *
+ * This is a "shadowing" view manager, which means that the {@link NativeViewHierarchyManager}
+ * will NOT manage children of native {@link TextView} instances instanciated by this manager.
+ * Instead we use @{link ReactBaseTextShadowNode} hierarchy to calculate a {@link Spannable} text
+ * represented the whole text subtree.
+ */
+public abstract class ReactTextAnchorViewManager
+ extends BaseViewManager {
+
+ private static final int[] SPACING_TYPES = {
+ Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
+ };
+
+ // maxLines can only be set in master view (block), doesn't really make sense to set in a span
+ @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = ViewDefaults.NUMBER_OF_LINES)
+ public void setNumberOfLines(ReactTextView view, int numberOfLines) {
+ view.setNumberOfLines(numberOfLines);
+ }
+
+ @ReactProp(name = ViewProps.ELLIPSIZE_MODE)
+ public void setEllipsizeMode(ReactTextView view, @Nullable String ellipsizeMode) {
+ if (ellipsizeMode == null || ellipsizeMode.equals("tail")) {
+ view.setEllipsizeLocation(TextUtils.TruncateAt.END);
+ } else if (ellipsizeMode.equals("head")) {
+ view.setEllipsizeLocation(TextUtils.TruncateAt.START);
+ } else if (ellipsizeMode.equals("middle")) {
+ view.setEllipsizeLocation(TextUtils.TruncateAt.MIDDLE);
+ } else {
+ throw new JSApplicationIllegalArgumentException("Invalid ellipsizeMode: " + ellipsizeMode);
+ }
+ }
+
+ @ReactProp(name = ViewProps.TEXT_ALIGN_VERTICAL)
+ public void setTextAlignVertical(ReactTextView view, @Nullable String textAlignVertical) {
+ if (textAlignVertical == null || "auto".equals(textAlignVertical)) {
+ view.setGravityVertical(Gravity.NO_GRAVITY);
+ } else if ("top".equals(textAlignVertical)) {
+ view.setGravityVertical(Gravity.TOP);
+ } else if ("bottom".equals(textAlignVertical)) {
+ view.setGravityVertical(Gravity.BOTTOM);
+ } else if ("center".equals(textAlignVertical)) {
+ view.setGravityVertical(Gravity.CENTER_VERTICAL);
+ } else {
+ throw new JSApplicationIllegalArgumentException(
+ "Invalid textAlignVertical: " + textAlignVertical);
+ }
+ }
+
+ @ReactProp(name = "selectable")
+ public void setSelectable(ReactTextView view, boolean isSelectable) {
+ view.setTextIsSelectable(isSelectable);
+ }
+
+ @ReactProp(name = "selectionColor", customType = "Color")
+ public void setSelectionColor(ReactTextView view, @Nullable Integer color) {
+ if (color == null) {
+ view.setHighlightColor(
+ DefaultStyleValuesUtil.getDefaultTextColorHighlight(view.getContext()));
+ } else {
+ view.setHighlightColor(color);
+ }
+ }
+
+ @ReactPropGroup(
+ names = {
+ ViewProps.BORDER_RADIUS,
+ ViewProps.BORDER_TOP_LEFT_RADIUS,
+ ViewProps.BORDER_TOP_RIGHT_RADIUS,
+ ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
+ ViewProps.BORDER_BOTTOM_LEFT_RADIUS
+ },
+ defaultFloat = YogaConstants.UNDEFINED
+ )
+ public void setBorderRadius(ReactTextView view, int index, float borderRadius) {
+ if (!YogaConstants.isUndefined(borderRadius)) {
+ borderRadius = PixelUtil.toPixelFromDIP(borderRadius);
+ }
+
+ if (index == 0) {
+ view.setBorderRadius(borderRadius);
+ } else {
+ view.setBorderRadius(borderRadius, index - 1);
+ }
+ }
+
+ @ReactProp(name = "borderStyle")
+ public void setBorderStyle(ReactTextView view, @Nullable String borderStyle) {
+ view.setBorderStyle(borderStyle);
+ }
+
+ @ReactPropGroup(
+ names = {
+ ViewProps.BORDER_WIDTH,
+ ViewProps.BORDER_LEFT_WIDTH,
+ ViewProps.BORDER_RIGHT_WIDTH,
+ ViewProps.BORDER_TOP_WIDTH,
+ ViewProps.BORDER_BOTTOM_WIDTH,
+ },
+ defaultFloat = YogaConstants.UNDEFINED
+ )
+ public void setBorderWidth(ReactTextView view, int index, float width) {
+ if (!YogaConstants.isUndefined(width)) {
+ width = PixelUtil.toPixelFromDIP(width);
+ }
+ view.setBorderWidth(SPACING_TYPES[index], width);
+ }
+
+ @ReactPropGroup(
+ names = {
+ "borderColor",
+ "borderLeftColor",
+ "borderRightColor",
+ "borderTopColor",
+ "borderBottomColor"
+ },
+ customType = "Color"
+ )
+ public void setBorderColor(ReactTextView view, int index, Integer color) {
+ float rgbComponent =
+ color == null ? YogaConstants.UNDEFINED : (float) ((int) color & 0x00FFFFFF);
+ float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int) color >>> 24);
+ view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent);
+ }
+
+ @ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
+ public void setIncludeFontPadding(ReactTextView view, boolean includepad) {
+ view.setIncludeFontPadding(includepad);
+ }
+
+ @ReactProp(name = "disabled", defaultBoolean = false)
+ public void setDisabled(ReactTextView view, boolean disabled) {
+ view.setEnabled(!disabled);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
index e95f5080e3e..7f9745d4861 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java
@@ -9,220 +9,40 @@
package com.facebook.react.views.text;
-import android.graphics.Typeface;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
-import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.BackgroundColorSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StrikethroughSpan;
-import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.widget.TextView;
import com.facebook.infer.annotation.Assertions;
-import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
-import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.common.annotations.VisibleForTesting;
-import com.facebook.react.uimanager.IllegalViewOperationException;
-import com.facebook.react.uimanager.LayoutShadowNode;
-import com.facebook.react.uimanager.PixelUtil;
-import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.UIViewOperationQueue;
-import com.facebook.react.uimanager.ViewDefaults;
-import com.facebook.react.uimanager.ViewProps;
-import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.yoga.YogaNode;
-import java.util.ArrayList;
-import java.util.List;
import javax.annotation.Nullable;
/**
- * {@link ReactShadowNode} class for spannable text view.
- *
- * This node calculates {@link Spannable} based on subnodes of the same type and passes the
- * resulting object down to textview's shadowview and actual native {@link TextView} instance. It is
- * important to keep in mind that {@link Spannable} is calculated only on layout step, so if there
- * are any text properties that may/should affect the result of {@link Spannable} they should be set
- * in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then then
- * passed as "computedDataFromMeasure" down to shadow and native view.
- *
+ * {@link ReactBaseTextShadowNode} concrete class for anchor {@code Text} node.
+ *
+ * The class measures text in {@code } view and feeds native {@link TextView} using {@code
+ * Spannable} object constructed in superclass.
*/
-public class ReactTextShadowNode extends LayoutShadowNode {
-
- private static final String INLINE_IMAGE_PLACEHOLDER = "I";
- public static final int UNSET = -1;
-
- @VisibleForTesting
- public static final String PROP_TEXT = "text";
-
- 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 int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
+public class ReactTextShadowNode extends ReactBaseTextShadowNode {
// It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it
// later by calling setFlags. This is because the latter approach triggers a bug on Android 4.4.2.
// The bug is that unicode emoticons aren't measured properly which causes text to be clipped.
private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
- private static class SetSpanOperation {
- protected int start, end;
- protected Object what;
- SetSpanOperation(int start, int end, Object what) {
- this.start = start;
- this.end = end;
- this.what = what;
- }
- public void execute(SpannableStringBuilder sb) {
- // All spans will automatically extend to the right of the text, but not the left - except
- // for spans that start at the beginning of the text.
- int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
- if (start == 0) {
- spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
- }
- sb.setSpan(what, start, end, spanFlags);
- }
- }
-
- private static void buildSpannedFromShadowNode(
- ReactTextShadowNode textShadowNode,
- SpannableStringBuilder sb,
- List ops) {
-
- int start = sb.length();
-
- for (int i = 0, length = textShadowNode.getChildCount(); i < length; i++) {
- ReactShadowNode child = textShadowNode.getChildAt(i);
-
- if (child instanceof ReactRawTextShadowNode) {
- sb.append(((ReactRawTextShadowNode) child).getText());
- } else if (child instanceof ReactTextShadowNode) {
- buildSpannedFromShadowNode((ReactTextShadowNode) child, sb, ops);
- } 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_IMAGE_PLACEHOLDER);
- ops.add(
- new SetSpanOperation(
- sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
- sb.length(),
- ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
- } else {
- throw new IllegalViewOperationException("Unexpected view type nested under text node: "
- + child.getClass());
- }
- child.markUpdateSeen();
- }
- int end = sb.length();
- if (end >= start) {
- if (textShadowNode.mIsColorSet) {
- ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textShadowNode.mColor)));
- }
- if (textShadowNode.mIsBackgroundColorSet) {
- ops.add(new SetSpanOperation(
- start,
- end,
- new BackgroundColorSpan(textShadowNode.mBackgroundColor)));
- }
- if (textShadowNode.mFontSize != UNSET) {
- ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textShadowNode.mFontSize)));
- }
- if (textShadowNode.mFontStyle != UNSET ||
- textShadowNode.mFontWeight != UNSET ||
- textShadowNode.mFontFamily != null) {
- ops.add(new SetSpanOperation(
- start,
- end,
- new CustomStyleSpan(
- textShadowNode.mFontStyle,
- textShadowNode.mFontWeight,
- textShadowNode.mFontFamily,
- textShadowNode.getThemedContext().getAssets())));
- }
- if (textShadowNode.mIsUnderlineTextDecorationSet) {
- ops.add(new SetSpanOperation(start, end, new UnderlineSpan()));
- }
- if (textShadowNode.mIsLineThroughTextDecorationSet) {
- ops.add(new SetSpanOperation(start, end, new StrikethroughSpan()));
- }
- if (textShadowNode.mTextShadowOffsetDx != 0 || textShadowNode.mTextShadowOffsetDy != 0) {
- ops.add(new SetSpanOperation(
- start,
- end,
- new ShadowStyleSpan(
- textShadowNode.mTextShadowOffsetDx,
- textShadowNode.mTextShadowOffsetDy,
- textShadowNode.mTextShadowRadius,
- textShadowNode.mTextShadowColor)));
- }
- if (!Float.isNaN(textShadowNode.getEffectiveLineHeight())) {
- ops.add(new SetSpanOperation(
- start,
- end,
- new CustomLineHeightSpan(textShadowNode.getEffectiveLineHeight())));
- }
- ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
- }
- }
-
- protected static Spannable spannedFromShadowNode(
- ReactTextShadowNode textShadowNode, String text) {
- 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 withing the region for which one may set
- // a new spannable will be wiped out
- List ops = new ArrayList<>();
- buildSpannedFromShadowNode(textShadowNode, sb, ops);
-
- if (text != null) {
- sb.append(text);
- }
-
- if (textShadowNode.mFontSize == UNSET) {
- sb.setSpan(
- new AbsoluteSizeSpan(textShadowNode.mAllowFontScaling
- ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
- : (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP))),
- 0,
- sb.length(),
- Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
- }
-
- textShadowNode.mContainsImages = false;
- textShadowNode.mHeightOfTallestInlineImage = Float.NaN;
-
- // While setting the Spans on the final text, we also check whether any of them are images
- for (int i = ops.size() - 1; i >= 0; i--) {
- SetSpanOperation op = ops.get(i);
- if (op.what instanceof TextInlineImageSpan) {
- int height = ((TextInlineImageSpan)op.what).getHeight();
- textShadowNode.mContainsImages = true;
- if (Float.isNaN(textShadowNode.mHeightOfTallestInlineImage) || height > textShadowNode.mHeightOfTallestInlineImage) {
- textShadowNode.mHeightOfTallestInlineImage = height;
- }
- }
- op.execute(sb);
- }
- return sb;
- }
+ private @Nullable Spannable mPreparedSpannableText;
private final YogaMeasureFunction mTextMeasureFunction =
new YogaMeasureFunction() {
@@ -318,89 +138,12 @@ public class ReactTextShadowNode extends LayoutShadowNode {
}
};
- /**
- * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
- * return the weight.
- *
- * This code is duplicated in ReactTextInputManager
- * TODO: Factor into a common place they can both use
- */
- private static int parseNumericFontWeight(String fontWeightString) {
- // This should be much faster than using regex to verify input and Integer.parseInt
- return fontWeightString.length() == 3 && fontWeightString.endsWith("00")
- && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ?
- 100 * (fontWeightString.charAt(0) - '0') : -1;
- }
-
- private float mLineHeight = Float.NaN;
- private boolean mIsColorSet = false;
- private boolean mAllowFontScaling = true;
- private int mColor;
- private boolean mIsBackgroundColorSet = false;
- private int mBackgroundColor;
-
- protected int mNumberOfLines = UNSET;
- protected int mFontSize = UNSET;
- protected float mFontSizeInput = UNSET;
- protected float mLineHeightInput = UNSET;
- protected int mTextAlign = Gravity.NO_GRAVITY;
- protected int mTextBreakStrategy = (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ?
- 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
-
- private float mTextShadowOffsetDx = 0;
- private float mTextShadowOffsetDy = 0;
- private float mTextShadowRadius = 1;
- private int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
-
- private boolean mIsUnderlineTextDecorationSet = false;
- private boolean mIsLineThroughTextDecorationSet = false;
- private boolean mIncludeFontPadding = true;
-
- /**
- * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
- * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
- */
- private int mFontStyle = UNSET;
- private int mFontWeight = UNSET;
- /**
- * NB: If a font family is used that does not have a style in a certain Android version (ie.
- * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
- * nodes. To retain that style, you have to add it to those nodes explicitly.
- * Example, Android 4.4:
- * Bold Text
- * Bold Text
- * Bold Text
- *
- * Not Bold Text
- * Not Bold Text
- * Not Bold Text
- *
- * Not Bold Text
- * Bold Text
- * Bold Text
- */
- private @Nullable String mFontFamily = null;
-
- private @Nullable Spannable mPreparedSpannableText;
-
- protected boolean mContainsImages = false;
- private float mHeightOfTallestInlineImage = Float.NaN;
-
public ReactTextShadowNode() {
if (!isVirtual()) {
setMeasureFunction(mTextMeasureFunction);
}
}
- // 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;
- }
-
// Return text alignment according to LTR or RTL style
private int getTextAlign() {
int textAlign = mTextAlign;
@@ -416,234 +159,19 @@ public class ReactTextShadowNode extends LayoutShadowNode {
@Override
public void onBeforeLayout() {
- if (isVirtual()) {
- return;
- }
mPreparedSpannableText = spannedFromShadowNode(this, null);
markUpdated();
}
- @Override
- public void markUpdated() {
- super.markUpdated();
- // We mark virtual anchor node as dirty as updated text needs to be re-measured
- if (!isVirtual()) {
- super.dirty();
- }
- }
-
- @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = UNSET)
- public void setNumberOfLines(int numberOfLines) {
- mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
- markUpdated();
- }
-
- @ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = UNSET)
- public void setLineHeight(float lineHeight) {
- mLineHeightInput = lineHeight;
- if (lineHeight == UNSET) {
- mLineHeight = Float.NaN;
- } else {
- mLineHeight = mAllowFontScaling ?
- PixelUtil.toPixelFromSP(lineHeight) : PixelUtil.toPixelFromDIP(lineHeight);
- }
- markUpdated();
- }
-
- @ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
- public void setAllowFontScaling(boolean allowFontScaling) {
- if (allowFontScaling != mAllowFontScaling) {
- mAllowFontScaling = allowFontScaling;
- setFontSize(mFontSizeInput);
- setLineHeight(mLineHeightInput);
- markUpdated();
- }
- }
-
- @ReactProp(name = ViewProps.TEXT_ALIGN)
- public void setTextAlign(@Nullable String textAlign) {
- 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 if ("justify".equals(textAlign)) {
- // Fallback gracefully for cross-platform compat instead of error
- mTextAlign = Gravity.LEFT;
- } else {
- throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
- }
- markUpdated();
- }
-
- @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = UNSET)
- public void setFontSize(float fontSize) {
- mFontSizeInput = fontSize;
- if (fontSize != UNSET) {
- fontSize = mAllowFontScaling ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
- : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
- }
- mFontSize = (int) fontSize;
- markUpdated();
- }
-
- @ReactProp(name = ViewProps.COLOR)
- public void setColor(@Nullable Integer color) {
- mIsColorSet = (color != null);
- if (mIsColorSet) {
- mColor = color;
- }
- markUpdated();
- }
-
- @ReactProp(name = ViewProps.BACKGROUND_COLOR)
- public void setBackgroundColor(Integer color) {
- // Don't apply background color to anchor TextView since it will be applied on the View directly
- if (!isVirtualAnchor()) {
- mIsBackgroundColorSet = (color != null);
- if (mIsBackgroundColorSet) {
- mBackgroundColor = color;
- }
- markUpdated();
- }
- }
-
- @ReactProp(name = ViewProps.FONT_FAMILY)
- public void setFontFamily(@Nullable String fontFamily) {
- mFontFamily = fontFamily;
- markUpdated();
- }
-
- /**
- /* This code is duplicated in ReactTextInputManager
- /* TODO: Factor into a common place they can both use
- */
- @ReactProp(name = ViewProps.FONT_WEIGHT)
- public void setFontWeight(@Nullable String fontWeightString) {
- int fontWeightNumeric = fontWeightString != null ?
- parseNumericFontWeight(fontWeightString) : -1;
- int fontWeight = UNSET;
- if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) {
- fontWeight = Typeface.BOLD;
- } else if ("normal".equals(fontWeightString) ||
- (fontWeightNumeric != -1 && fontWeightNumeric < 500)) {
- fontWeight = Typeface.NORMAL;
- }
- if (fontWeight != mFontWeight) {
- mFontWeight = fontWeight;
- markUpdated();
- }
- }
-
- /**
- /* This code is duplicated in ReactTextInputManager
- /* TODO: Factor into a common place they can both use
- */
- @ReactProp(name = ViewProps.FONT_STYLE)
- public void setFontStyle(@Nullable String fontStyleString) {
- int fontStyle = UNSET;
- if ("italic".equals(fontStyleString)) {
- fontStyle = Typeface.ITALIC;
- } else if ("normal".equals(fontStyleString)) {
- fontStyle = Typeface.NORMAL;
- }
- 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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- return;
- }
-
- 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 {
- throw new JSApplicationIllegalArgumentException("Invalid textBreakStrategy: " + textBreakStrategy);
- }
-
- 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();
- }
- }
-
@Override
public boolean isVirtualAnchor() {
- return !isVirtual();
+ return true;
}
@Override
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
- if (isVirtual()) {
- return;
- }
super.onCollectExtraUpdates(uiViewOperationQueue);
+
if (mPreparedSpannableText != null) {
ReactTextUpdate reactTextUpdate =
new ReactTextUpdate(
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java
index 392ee1d30c3..2ade0ece3fd 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java
@@ -10,43 +10,21 @@
package com.facebook.react.views.text;
import android.text.Spannable;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.widget.TextView;
-
-import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.module.annotations.ReactModule;
-import com.facebook.react.uimanager.BaseViewManager;
-import com.facebook.react.uimanager.PixelUtil;
-import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ThemedReactContext;
-import com.facebook.react.uimanager.ViewDefaults;
-import com.facebook.react.uimanager.ViewProps;
-import com.facebook.react.uimanager.annotations.ReactProp;
-import com.facebook.react.uimanager.annotations.ReactPropGroup;
-import com.facebook.yoga.YogaConstants;
-
-import javax.annotation.Nullable;
/**
- * Manages instances of spannable {@link TextView}.
- *
- * This is a "shadowing" view manager, which means that the {@link NativeViewHierarchyManager} will
- * not manage children of native {@link TextView} instances returned by this manager. Instead we use
- * @{link ReactTextShadowNode} hierarchy to calculate a {@link Spannable} text representing the
- * whole text subtree.
+ * Concrete class for {@link ReactTextAnchorViewManager} which represents view managers of anchor
+ * {@code } nodes.
*/
@ReactModule(name = ReactTextViewManager.REACT_CLASS)
-public class ReactTextViewManager extends BaseViewManager {
+public class ReactTextViewManager
+ extends ReactTextAnchorViewManager {
@VisibleForTesting
public static final String REACT_CLASS = "RCTText";
- private static final int[] SPACING_TYPES = {
- Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
- };
-
@Override
public String getName() {
return REACT_CLASS;
@@ -57,111 +35,6 @@ public class ReactTextViewManager extends BaseViewManager>> 24);
- view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent);
- }
-
- @ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
- public void setIncludeFontPadding(ReactTextView view, boolean includepad) {
- view.setIncludeFontPadding(includepad);
- }
-
- @ReactProp(name = "disabled", defaultBoolean = false)
- public void setDisabled(ReactTextView view, boolean disabled) {
- view.setEnabled(!disabled);
- }
-
@Override
public void updateExtraData(ReactTextView view, Object extraData) {
ReactTextUpdate update = (ReactTextUpdate) extraData;
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextShadowNode.java
index a5572a421fa..734549aebb8 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextShadowNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextShadowNode.java
@@ -3,9 +3,9 @@
package com.facebook.react.views.text;
/**
- * A virtual text node. Should only be a child of a ReactTextShadowNode.
+ * A virtual text node.
*/
-public class ReactVirtualTextShadowNode extends ReactTextShadowNode {
+public class ReactVirtualTextShadowNode extends ReactBaseTextShadowNode {
@Override
public boolean isVirtual() {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java
index c86439c2b05..64753b8a752 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java
@@ -9,8 +9,10 @@
package com.facebook.react.views.text;
+import android.view.View;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.module.annotations.ReactModule;
+import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
/**
@@ -18,7 +20,7 @@ import com.facebook.react.uimanager.ThemedReactContext;
* operation will throw an {@link IllegalStateException}
*/
@ReactModule(name = ReactVirtualTextViewManager.REACT_CLASS)
-public class ReactVirtualTextViewManager extends ReactTextViewManager {
+public class ReactVirtualTextViewManager extends BaseViewManager {
@VisibleForTesting
public static final String REACT_CLASS = "RCTVirtualText";
@@ -29,15 +31,20 @@ public class ReactVirtualTextViewManager extends ReactTextViewManager {
}
@Override
- public ReactTextView createViewInstance(ThemedReactContext context) {
+ public View createViewInstance(ThemedReactContext context) {
throw new IllegalStateException("Attempt to create a native view for RCTVirtualText");
}
@Override
- public void updateExtraData(ReactTextView view, Object extraData) {}
+ public void updateExtraData(View view, Object extraData) {}
@Override
- public ReactTextShadowNode createShadowNodeInstance() {
+ public Class getShadowNodeClass() {
+ return ReactVirtualTextShadowNode.class;
+ }
+
+ @Override
+ public ReactVirtualTextShadowNode createShadowNodeInstance() {
return new ReactVirtualTextShadowNode();
}
}
diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java
index c0c22e16619..0dc5a25ce27 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java
+++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java
@@ -35,7 +35,7 @@ import com.facebook.react.bridge.ReactTestHelper;
import com.facebook.react.modules.core.ChoreographerCompat;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.views.text.ReactRawTextManager;
-import com.facebook.react.views.text.ReactTextShadowNode;
+import com.facebook.react.views.text.ReactRawTextShadowNode;
import com.facebook.react.views.text.ReactTextViewManager;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.view.ReactViewManager;
@@ -134,7 +134,7 @@ public class UIManagerModuleTest {
uiManager.updateView(
rawTextTag,
ReactRawTextManager.REACT_CLASS,
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "New text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "New text"));
uiManager.onBatchComplete();
executePendingFrameCallbacks();
@@ -672,7 +672,7 @@ public class UIManagerModuleTest {
rawTextTag,
ReactRawTextManager.REACT_CLASS,
rootTag,
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, text, "collapsable", false));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, text, "collapsable", false));
uiManager.manageChildren(
textTag,
diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java
index 2834ac14cd0..49755d2ccb6 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java
+++ b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java
@@ -37,6 +37,7 @@ import com.facebook.react.uimanager.UIImplementationProvider;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.views.text.ReactRawTextShadowNode;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -100,7 +101,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_SIZE, 21.0),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
AbsoluteSizeSpan sizeSpan = getSingleSpan(
(TextView) rootView.getChildAt(0), AbsoluteSizeSpan.class);
@@ -114,7 +115,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -129,7 +130,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "500"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -144,7 +145,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_STYLE, "italic"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -159,7 +160,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold", ViewProps.FONT_STYLE, "italic"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -174,7 +175,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "normal"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -188,7 +189,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "200"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -202,7 +203,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_STYLE, "normal"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -216,7 +217,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -232,7 +233,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_WEIGHT, "bold"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -248,7 +249,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_STYLE, "italic"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -267,7 +268,7 @@ public class ReactTextTest {
ViewProps.FONT_FAMILY, "sans-serif",
ViewProps.FONT_WEIGHT, "500",
ViewProps.FONT_STYLE, "italic"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
CustomStyleSpan customStyleSpan =
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
@@ -283,7 +284,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
TextView textView = (TextView) rootView.getChildAt(0);
Spanned text = (Spanned) textView.getText();
@@ -301,7 +302,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "line-through"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
TextView textView = (TextView) rootView.getChildAt(0);
Spanned text = (Spanned) textView.getText();
@@ -320,7 +321,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline line-through"),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
UnderlineSpan underlineSpan =
getSingleSpan((TextView) rootView.getChildAt(0), UnderlineSpan.class);
@@ -337,7 +338,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.BACKGROUND_COLOR, Color.BLUE),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
Drawable backgroundDrawable = ((TextView) rootView.getChildAt(0)).getBackground();
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
@@ -353,7 +354,7 @@ public class ReactTextTest {
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.NUMBER_OF_LINES, 2),
- JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
+ JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
TextView textView = (TextView) rootView.getChildAt(0);
assertThat(textView.getText().toString()).isEqualTo("test text");