mirror of
https://github.com/status-im/react-native.git
synced 2025-02-27 00:20:31 +00:00
Restructured inheritance around ReactTextViewManager and co.
Summary: Abstract class `ReactBaseTextShadowNode` was decoupled from `ReactTextShadowNode` to separate two goals/roles: * `ReactBaseTextShadowNode` represents spanned `<Text>` nodes, which can bear text attributes (both `RCTText` and `RCTVirtualText`); * `ReactTextShadowNode` represents anchor `<Text>` 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 `<TextInput>` component using right inheritance. Yay! Reviewed By: achen1 Differential Revision: D5715830 fbshipit-source-id: ecc0764a03b5b7586fe77ad31f149cd840f4da41
This commit is contained in:
parent
80027ce6db
commit
6114f863c3
@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
package com.facebook.react.flat;
|
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.annotation.TargetApi;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -17,11 +18,6 @@ import android.text.SpannableStringBuilder;
|
|||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
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.infer.annotation.Assertions;
|
||||||
import com.facebook.react.uimanager.PixelUtil;
|
import com.facebook.react.uimanager.PixelUtil;
|
||||||
import com.facebook.react.uimanager.Spacing;
|
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.uimanager.annotations.ReactProp;
|
||||||
import com.facebook.react.views.text.ReactTextUpdate;
|
import com.facebook.react.views.text.ReactTextUpdate;
|
||||||
import com.facebook.react.views.view.MeasureUtil;
|
import com.facebook.react.views.view.MeasureUtil;
|
||||||
|
import com.facebook.yoga.YogaMeasureFunction;
|
||||||
import static com.facebook.react.views.text.ReactTextShadowNode.PROP_TEXT;
|
import com.facebook.yoga.YogaMeasureMode;
|
||||||
import static com.facebook.react.views.text.ReactTextShadowNode.UNSET;
|
import com.facebook.yoga.YogaMeasureOutput;
|
||||||
|
import com.facebook.yoga.YogaNode;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class RCTTextInput extends RCTVirtualText implements AndroidView, YogaMeasureFunction {
|
public class RCTTextInput extends RCTVirtualText implements AndroidView, YogaMeasureFunction {
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>This class handles all text attributes assosiated with {@code <Text>}-ish node. A concrete
|
||||||
|
* node can be an anchor {@code <Text>} node, an anchor {@code <TextInput>} node or virtual {@code
|
||||||
|
* <Text>} node inside {@code <Text>} or {@code <TextInput>} node. Or even something else.
|
||||||
|
*
|
||||||
|
* <p>This also node calculates {@link Spannable} object based on subnodes of the same type, which
|
||||||
|
* can be used in concrete classes to feed native views and compute layout.
|
||||||
|
*/
|
||||||
|
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<SetSpanOperation> 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<SetSpanOperation> 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:
|
||||||
|
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="serif}}>Bold Text</Text>
|
||||||
|
*
|
||||||
|
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
|
||||||
|
*
|
||||||
|
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
|
||||||
|
* <Text style={{fontFamily="serif}}>Bold Text</Text>
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* <p>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 <Text>}-ish spannable views, such as {@link TextView} or {@link
|
||||||
|
* TextEdit}.
|
||||||
|
*
|
||||||
|
* <p>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<T extends View, C extends ReactBaseTextShadowNode>
|
||||||
|
extends BaseViewManager<T, C> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -9,220 +9,40 @@
|
|||||||
|
|
||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.text;
|
||||||
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.BoringLayout;
|
import android.text.BoringLayout;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.StaticLayout;
|
import android.text.StaticLayout;
|
||||||
import android.text.TextPaint;
|
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.view.Gravity;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import com.facebook.infer.annotation.Assertions;
|
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.Spacing;
|
||||||
import com.facebook.react.uimanager.UIViewOperationQueue;
|
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.YogaConstants;
|
||||||
import com.facebook.yoga.YogaDirection;
|
import com.facebook.yoga.YogaDirection;
|
||||||
import com.facebook.yoga.YogaMeasureFunction;
|
import com.facebook.yoga.YogaMeasureFunction;
|
||||||
import com.facebook.yoga.YogaMeasureMode;
|
import com.facebook.yoga.YogaMeasureMode;
|
||||||
import com.facebook.yoga.YogaMeasureOutput;
|
import com.facebook.yoga.YogaMeasureOutput;
|
||||||
import com.facebook.yoga.YogaNode;
|
import com.facebook.yoga.YogaNode;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ReactShadowNode} class for spannable text view.
|
* {@link ReactBaseTextShadowNode} concrete class for anchor {@code Text} node.
|
||||||
* <p/>
|
*
|
||||||
* This node calculates {@link Spannable} based on subnodes of the same type and passes the
|
* <p>The class measures text in {@code <Text>} view and feeds native {@link TextView} using {@code
|
||||||
* resulting object down to textview's shadowview and actual native {@link TextView} instance. It is
|
* Spannable} object constructed in superclass.
|
||||||
* 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.
|
|
||||||
* <p/>
|
|
||||||
*/
|
*/
|
||||||
public class ReactTextShadowNode extends LayoutShadowNode {
|
public class ReactTextShadowNode extends ReactBaseTextShadowNode {
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it
|
// 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.
|
// 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.
|
// 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 final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
private static class SetSpanOperation {
|
private @Nullable Spannable mPreparedSpannableText;
|
||||||
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<SetSpanOperation> 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<SetSpanOperation> 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 final YogaMeasureFunction mTextMeasureFunction =
|
private final YogaMeasureFunction mTextMeasureFunction =
|
||||||
new YogaMeasureFunction() {
|
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:
|
|
||||||
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="serif}}>Bold Text</Text>
|
|
||||||
*
|
|
||||||
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
|
|
||||||
*
|
|
||||||
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
|
|
||||||
* <Text style={{fontFamily="serif}}>Bold Text</Text>
|
|
||||||
*/
|
|
||||||
private @Nullable String mFontFamily = null;
|
|
||||||
|
|
||||||
private @Nullable Spannable mPreparedSpannableText;
|
|
||||||
|
|
||||||
protected boolean mContainsImages = false;
|
|
||||||
private float mHeightOfTallestInlineImage = Float.NaN;
|
|
||||||
|
|
||||||
public ReactTextShadowNode() {
|
public ReactTextShadowNode() {
|
||||||
if (!isVirtual()) {
|
if (!isVirtual()) {
|
||||||
setMeasureFunction(mTextMeasureFunction);
|
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
|
// Return text alignment according to LTR or RTL style
|
||||||
private int getTextAlign() {
|
private int getTextAlign() {
|
||||||
int textAlign = mTextAlign;
|
int textAlign = mTextAlign;
|
||||||
@ -416,234 +159,19 @@ public class ReactTextShadowNode extends LayoutShadowNode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBeforeLayout() {
|
public void onBeforeLayout() {
|
||||||
if (isVirtual()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mPreparedSpannableText = spannedFromShadowNode(this, null);
|
mPreparedSpannableText = spannedFromShadowNode(this, null);
|
||||||
markUpdated();
|
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
|
@Override
|
||||||
public boolean isVirtualAnchor() {
|
public boolean isVirtualAnchor() {
|
||||||
return !isVirtual();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
|
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
|
||||||
if (isVirtual()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.onCollectExtraUpdates(uiViewOperationQueue);
|
super.onCollectExtraUpdates(uiViewOperationQueue);
|
||||||
|
|
||||||
if (mPreparedSpannableText != null) {
|
if (mPreparedSpannableText != null) {
|
||||||
ReactTextUpdate reactTextUpdate =
|
ReactTextUpdate reactTextUpdate =
|
||||||
new ReactTextUpdate(
|
new ReactTextUpdate(
|
||||||
|
@ -10,43 +10,21 @@
|
|||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.text;
|
||||||
|
|
||||||
import android.text.Spannable;
|
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.common.annotations.VisibleForTesting;
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
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.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}.
|
* Concrete class for {@link ReactTextAnchorViewManager} which represents view managers of anchor
|
||||||
*
|
* {@code <Text>} nodes.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@ReactModule(name = ReactTextViewManager.REACT_CLASS)
|
@ReactModule(name = ReactTextViewManager.REACT_CLASS)
|
||||||
public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTextShadowNode> {
|
public class ReactTextViewManager
|
||||||
|
extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode> {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static final String REACT_CLASS = "RCTText";
|
public static final String REACT_CLASS = "RCTText";
|
||||||
|
|
||||||
private static final int[] SPACING_TYPES = {
|
|
||||||
Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return REACT_CLASS;
|
return REACT_CLASS;
|
||||||
@ -57,111 +35,6 @@ public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTe
|
|||||||
return new ReactTextView(context);
|
return new ReactTextView(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateExtraData(ReactTextView view, Object extraData) {
|
public void updateExtraData(ReactTextView view, Object extraData) {
|
||||||
ReactTextUpdate update = (ReactTextUpdate) extraData;
|
ReactTextUpdate update = (ReactTextUpdate) extraData;
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
package com.facebook.react.views.text;
|
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
|
@Override
|
||||||
public boolean isVirtual() {
|
public boolean isVirtual() {
|
||||||
|
@ -9,8 +9,10 @@
|
|||||||
|
|
||||||
package com.facebook.react.views.text;
|
package com.facebook.react.views.text;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
import com.facebook.react.uimanager.BaseViewManager;
|
||||||
import com.facebook.react.uimanager.ThemedReactContext;
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,7 +20,7 @@ import com.facebook.react.uimanager.ThemedReactContext;
|
|||||||
* operation will throw an {@link IllegalStateException}
|
* operation will throw an {@link IllegalStateException}
|
||||||
*/
|
*/
|
||||||
@ReactModule(name = ReactVirtualTextViewManager.REACT_CLASS)
|
@ReactModule(name = ReactVirtualTextViewManager.REACT_CLASS)
|
||||||
public class ReactVirtualTextViewManager extends ReactTextViewManager {
|
public class ReactVirtualTextViewManager extends BaseViewManager<View, ReactVirtualTextShadowNode> {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static final String REACT_CLASS = "RCTVirtualText";
|
public static final String REACT_CLASS = "RCTVirtualText";
|
||||||
@ -29,15 +31,20 @@ public class ReactVirtualTextViewManager extends ReactTextViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReactTextView createViewInstance(ThemedReactContext context) {
|
public View createViewInstance(ThemedReactContext context) {
|
||||||
throw new IllegalStateException("Attempt to create a native view for RCTVirtualText");
|
throw new IllegalStateException("Attempt to create a native view for RCTVirtualText");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateExtraData(ReactTextView view, Object extraData) {}
|
public void updateExtraData(View view, Object extraData) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReactTextShadowNode createShadowNodeInstance() {
|
public Class<ReactVirtualTextShadowNode> getShadowNodeClass() {
|
||||||
|
return ReactVirtualTextShadowNode.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReactVirtualTextShadowNode createShadowNodeInstance() {
|
||||||
return new ReactVirtualTextShadowNode();
|
return new ReactVirtualTextShadowNode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import com.facebook.react.bridge.ReactTestHelper;
|
|||||||
import com.facebook.react.modules.core.ChoreographerCompat;
|
import com.facebook.react.modules.core.ChoreographerCompat;
|
||||||
import com.facebook.react.modules.core.ReactChoreographer;
|
import com.facebook.react.modules.core.ReactChoreographer;
|
||||||
import com.facebook.react.views.text.ReactRawTextManager;
|
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.text.ReactTextViewManager;
|
||||||
import com.facebook.react.views.view.ReactViewGroup;
|
import com.facebook.react.views.view.ReactViewGroup;
|
||||||
import com.facebook.react.views.view.ReactViewManager;
|
import com.facebook.react.views.view.ReactViewManager;
|
||||||
@ -134,7 +134,7 @@ public class UIManagerModuleTest {
|
|||||||
uiManager.updateView(
|
uiManager.updateView(
|
||||||
rawTextTag,
|
rawTextTag,
|
||||||
ReactRawTextManager.REACT_CLASS,
|
ReactRawTextManager.REACT_CLASS,
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "New text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "New text"));
|
||||||
|
|
||||||
uiManager.onBatchComplete();
|
uiManager.onBatchComplete();
|
||||||
executePendingFrameCallbacks();
|
executePendingFrameCallbacks();
|
||||||
@ -672,7 +672,7 @@ public class UIManagerModuleTest {
|
|||||||
rawTextTag,
|
rawTextTag,
|
||||||
ReactRawTextManager.REACT_CLASS,
|
ReactRawTextManager.REACT_CLASS,
|
||||||
rootTag,
|
rootTag,
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, text, "collapsable", false));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, text, "collapsable", false));
|
||||||
|
|
||||||
uiManager.manageChildren(
|
uiManager.manageChildren(
|
||||||
textTag,
|
textTag,
|
||||||
|
@ -37,6 +37,7 @@ import com.facebook.react.uimanager.UIImplementationProvider;
|
|||||||
import com.facebook.react.uimanager.UIManagerModule;
|
import com.facebook.react.uimanager.UIManagerModule;
|
||||||
import com.facebook.react.uimanager.ViewManager;
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
import com.facebook.react.uimanager.ViewProps;
|
import com.facebook.react.uimanager.ViewProps;
|
||||||
|
import com.facebook.react.views.text.ReactRawTextShadowNode;
|
||||||
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
|
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -100,7 +101,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_SIZE, 21.0),
|
JavaOnlyMap.of(ViewProps.FONT_SIZE, 21.0),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
AbsoluteSizeSpan sizeSpan = getSingleSpan(
|
AbsoluteSizeSpan sizeSpan = getSingleSpan(
|
||||||
(TextView) rootView.getChildAt(0), AbsoluteSizeSpan.class);
|
(TextView) rootView.getChildAt(0), AbsoluteSizeSpan.class);
|
||||||
@ -114,7 +115,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold"),
|
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -129,7 +130,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "500"),
|
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "500"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -144,7 +145,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_STYLE, "italic"),
|
JavaOnlyMap.of(ViewProps.FONT_STYLE, "italic"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -159,7 +160,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold", ViewProps.FONT_STYLE, "italic"),
|
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 =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -174,7 +175,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "normal"),
|
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "normal"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -188,7 +189,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "200"),
|
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "200"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -202,7 +203,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_STYLE, "normal"),
|
JavaOnlyMap.of(ViewProps.FONT_STYLE, "normal"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -216,7 +217,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif"),
|
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -232,7 +233,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_WEIGHT, "bold"),
|
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 =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -248,7 +249,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_STYLE, "italic"),
|
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 =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -267,7 +268,7 @@ public class ReactTextTest {
|
|||||||
ViewProps.FONT_FAMILY, "sans-serif",
|
ViewProps.FONT_FAMILY, "sans-serif",
|
||||||
ViewProps.FONT_WEIGHT, "500",
|
ViewProps.FONT_WEIGHT, "500",
|
||||||
ViewProps.FONT_STYLE, "italic"),
|
ViewProps.FONT_STYLE, "italic"),
|
||||||
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
|
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text"));
|
||||||
|
|
||||||
CustomStyleSpan customStyleSpan =
|
CustomStyleSpan customStyleSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class);
|
||||||
@ -283,7 +284,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline"),
|
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);
|
TextView textView = (TextView) rootView.getChildAt(0);
|
||||||
Spanned text = (Spanned) textView.getText();
|
Spanned text = (Spanned) textView.getText();
|
||||||
@ -301,7 +302,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "line-through"),
|
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);
|
TextView textView = (TextView) rootView.getChildAt(0);
|
||||||
Spanned text = (Spanned) textView.getText();
|
Spanned text = (Spanned) textView.getText();
|
||||||
@ -320,7 +321,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline line-through"),
|
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 =
|
UnderlineSpan underlineSpan =
|
||||||
getSingleSpan((TextView) rootView.getChildAt(0), UnderlineSpan.class);
|
getSingleSpan((TextView) rootView.getChildAt(0), UnderlineSpan.class);
|
||||||
@ -337,7 +338,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.BACKGROUND_COLOR, Color.BLUE),
|
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();
|
Drawable backgroundDrawable = ((TextView) rootView.getChildAt(0)).getBackground();
|
||||||
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
|
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
|
||||||
@ -353,7 +354,7 @@ public class ReactTextTest {
|
|||||||
ReactRootView rootView = createText(
|
ReactRootView rootView = createText(
|
||||||
uiManager,
|
uiManager,
|
||||||
JavaOnlyMap.of(ViewProps.NUMBER_OF_LINES, 2),
|
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);
|
TextView textView = (TextView) rootView.getChildAt(0);
|
||||||
assertThat(textView.getText().toString()).isEqualTo("test text");
|
assertThat(textView.getText().toString()).isEqualTo("test text");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user