From 6114f863c3aed826355530bcf404ee5aed2f927b Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 11 Sep 2017 15:44:41 -0700 Subject: [PATCH] Restructured inheritance around ReactTextViewManager and co. Summary: Abstract class `ReactBaseTextShadowNode` was decoupled from `ReactTextShadowNode` to separate two goals/roles: * `ReactBaseTextShadowNode` represents spanned `` nodes, which can bear text attributes (both `RCTText` and `RCTVirtualText`); * `ReactTextShadowNode` represents anchor `` view in Yoga terms, which can bear layout attributes (`RCTText` and `RCTTextInput`). `ReactVirtualTextShadowNode` now inherits `ReactBaseTextShadowNode`. The same architectural changes was applited to view managers. Why? * This is just a better architecture which represents the nature of this objects. * Bunch of "negative" logic which turn off excessive features for some suclasses was removed. * Memory efficiency. * Now we can improve `` component using right inheritance. Yay! Reviewed By: achen1 Differential Revision: D5715830 fbshipit-source-id: ecc0764a03b5b7586fe77ad31f149cd840f4da41 --- .../com/facebook/react/flat/RCTTextInput.java | 16 +- .../views/text/ReactBaseTextShadowNode.java | 504 ++++++++++++++++++ .../text/ReactTextAnchorViewManager.java | 163 ++++++ .../react/views/text/ReactTextShadowNode.java | 488 +---------------- .../views/text/ReactTextViewManager.java | 135 +---- .../text/ReactVirtualTextShadowNode.java | 4 +- .../text/ReactVirtualTextViewManager.java | 15 +- .../react/uimanager/UIManagerModuleTest.java | 6 +- .../react/views/text/ReactTextTest.java | 35 +- 9 files changed, 720 insertions(+), 646 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java 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 2af86a5a73..221117a6c8 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 0000000000..7dc50f710c --- /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 0000000000..22eba4e915 --- /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 e95f5080e3..7f9745d486 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 392ee1d30c..2ade0ece3f 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 a5572a421f..734549aebb 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 c86439c2b0..64753b8a75 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 c0c22e1661..0dc5a25ce2 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 2834ac14cd..49755d2ccb 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");