mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-03 22:48:25 +08:00
Summary: Nodes wasn't supporting text decorations to the line (strike through and underline). This patch implements that. Differential Revision: D3512711
312 lines
10 KiB
Java
312 lines
10 KiB
Java
/**
|
|
* 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.flat;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import android.graphics.Typeface;
|
|
import android.text.Spannable;
|
|
import android.text.SpannableStringBuilder;
|
|
import android.text.TextUtils;
|
|
|
|
import com.facebook.csslayout.CSSNode;
|
|
import com.facebook.react.bridge.ReadableMap;
|
|
import com.facebook.react.uimanager.PixelUtil;
|
|
import com.facebook.react.uimanager.ViewProps;
|
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
|
|
/**
|
|
* RCTVirtualText is a {@link FlatTextShadowNode} that can contain font styling information.
|
|
*/
|
|
/* package */ class RCTVirtualText extends FlatTextShadowNode {
|
|
|
|
private static final String BOLD = "bold";
|
|
private static final String ITALIC = "italic";
|
|
private static final String NORMAL = "normal";
|
|
|
|
private static final String PROP_SHADOW_OFFSET = "textShadowOffset";
|
|
private static final String PROP_SHADOW_RADIUS = "textShadowRadius";
|
|
private static final String PROP_SHADOW_COLOR = "textShadowColor";
|
|
private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
|
|
|
|
private FontStylingSpan mFontStylingSpan = FontStylingSpan.INSTANCE;
|
|
private ShadowStyleSpan mShadowStyleSpan = ShadowStyleSpan.INSTANCE;
|
|
|
|
@Override
|
|
public void addChildAt(CSSNode child, int i) {
|
|
super.addChildAt(child, i);
|
|
notifyChanged(true);
|
|
}
|
|
|
|
@Override
|
|
protected void performCollectText(SpannableStringBuilder builder) {
|
|
for (int i = 0, childCount = getChildCount(); i < childCount; ++i) {
|
|
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
|
child.collectText(builder);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void performApplySpans(SpannableStringBuilder builder, int begin, int end, boolean isEditable) {
|
|
mFontStylingSpan.freeze();
|
|
|
|
// 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.
|
|
final int flag;
|
|
if (isEditable) {
|
|
flag = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
|
|
} else {
|
|
flag = begin == 0 ?
|
|
Spannable.SPAN_INCLUSIVE_INCLUSIVE :
|
|
Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
|
|
}
|
|
|
|
builder.setSpan(
|
|
mFontStylingSpan,
|
|
begin,
|
|
end,
|
|
flag);
|
|
|
|
if (mShadowStyleSpan.getColor() != 0 && mShadowStyleSpan.getRadius() != 0) {
|
|
mShadowStyleSpan.freeze();
|
|
|
|
builder.setSpan(
|
|
mShadowStyleSpan,
|
|
begin,
|
|
end,
|
|
flag);
|
|
}
|
|
|
|
for (int i = 0, childCount = getChildCount(); i < childCount; ++i) {
|
|
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
|
child.applySpans(builder, isEditable);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void performCollectAttachDetachListeners(StateBuilder stateBuilder) {
|
|
for (int i = 0, childCount = getChildCount(); i < childCount; ++i) {
|
|
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
|
child.performCollectAttachDetachListeners(stateBuilder);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = Float.NaN)
|
|
public void setFontSize(float fontSizeSp) {
|
|
final int fontSize;
|
|
if (Float.isNaN(fontSizeSp)) {
|
|
fontSize = getDefaultFontSize();
|
|
} else {
|
|
fontSize = fontSizeFromSp(fontSizeSp);
|
|
}
|
|
|
|
if (mFontStylingSpan.getFontSize() != fontSize) {
|
|
getSpan().setFontSize(fontSize);
|
|
notifyChanged(true);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.COLOR, defaultDouble = Double.NaN)
|
|
public void setColor(double textColor) {
|
|
if (mFontStylingSpan.getTextColor() != textColor) {
|
|
getSpan().setTextColor(textColor);
|
|
notifyChanged(false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setBackgroundColor(int backgroundColor) {
|
|
if (isVirtual()) {
|
|
// for nested Text elements, we want to apply background color to the text only
|
|
// e.g. Hello <style backgroundColor=red>World</style>, "World" will have red background color
|
|
if (mFontStylingSpan.getBackgroundColor() != backgroundColor) {
|
|
getSpan().setBackgroundColor(backgroundColor);
|
|
notifyChanged(false);
|
|
}
|
|
} else {
|
|
// for top-level Text element, background needs to be applied for the entire shadow node
|
|
//
|
|
// For example: <Text style={flex:1}>Hello World</Text>
|
|
// "Hello World" may only occupy e.g. 200 pixels, but the node may be measured at e.g. 500px.
|
|
// In this case, we want background to be 500px wide as well, and this is exactly what
|
|
// FlatShadowNode does.
|
|
super.setBackgroundColor(backgroundColor);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.FONT_FAMILY)
|
|
public void setFontFamily(@Nullable String fontFamily) {
|
|
if (!TextUtils.equals(mFontStylingSpan.getFontFamily(), fontFamily)) {
|
|
getSpan().setFontFamily(fontFamily);
|
|
notifyChanged(true);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.FONT_WEIGHT)
|
|
public void setFontWeight(@Nullable String fontWeightString) {
|
|
final int fontWeight;
|
|
if (fontWeightString == null) {
|
|
fontWeight = -1;
|
|
} else if (BOLD.equals(fontWeightString)) {
|
|
fontWeight = Typeface.BOLD;
|
|
} else if (NORMAL.equals(fontWeightString)) {
|
|
fontWeight = Typeface.NORMAL;
|
|
} else {
|
|
int fontWeightNumeric = parseNumericFontWeight(fontWeightString);
|
|
if (fontWeightNumeric == -1) {
|
|
throw new RuntimeException("invalid font weight " + fontWeightString);
|
|
}
|
|
fontWeight = fontWeightNumeric >= 500 ? Typeface.BOLD : Typeface.NORMAL;
|
|
}
|
|
|
|
if (mFontStylingSpan.getFontWeight() != fontWeight) {
|
|
getSpan().setFontWeight(fontWeight);
|
|
notifyChanged(true);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.TEXT_DECORATION_LINE)
|
|
public void setTextDecorationLine(@Nullable String textDecorationLineString) {
|
|
boolean isUnderlineTextDecorationSet = false;
|
|
boolean isLineThroughTextDecorationSet = false;
|
|
if (textDecorationLineString != null) {
|
|
for (String textDecorationLineSubString : textDecorationLineString.split(" ")) {
|
|
if ("underline".equals(textDecorationLineSubString)) {
|
|
isUnderlineTextDecorationSet = true;
|
|
} else if ("line-through".equals(textDecorationLineSubString)) {
|
|
isLineThroughTextDecorationSet = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isUnderlineTextDecorationSet != mFontStylingSpan.hasUnderline() ||
|
|
isLineThroughTextDecorationSet != mFontStylingSpan.hasStrikeThrough()) {
|
|
FontStylingSpan span = getSpan();
|
|
span.setHasUnderline(isUnderlineTextDecorationSet);
|
|
span.setHasStrikeThrough(isLineThroughTextDecorationSet);
|
|
notifyChanged(true);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.FONT_STYLE)
|
|
public void setFontStyle(@Nullable String fontStyleString) {
|
|
final int fontStyle;
|
|
if (fontStyleString == null) {
|
|
fontStyle = -1;
|
|
} else if (ITALIC.equals(fontStyleString)) {
|
|
fontStyle = Typeface.ITALIC;
|
|
} else if (NORMAL.equals(fontStyleString)) {
|
|
fontStyle = Typeface.NORMAL;
|
|
} else {
|
|
throw new RuntimeException("invalid font style " + fontStyleString);
|
|
}
|
|
|
|
if (mFontStylingSpan.getFontStyle() != fontStyle) {
|
|
getSpan().setFontStyle(fontStyle);
|
|
notifyChanged(true);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = PROP_SHADOW_OFFSET)
|
|
public void setTextShadowOffset(@Nullable ReadableMap offsetMap) {
|
|
float dx = 0;
|
|
float dy = 0;
|
|
if (offsetMap != null) {
|
|
if (offsetMap.hasKey("width")) {
|
|
dx = PixelUtil.toPixelFromDIP(offsetMap.getDouble("width"));
|
|
}
|
|
if (offsetMap.hasKey("height")) {
|
|
dy = PixelUtil.toPixelFromDIP(offsetMap.getDouble("height"));
|
|
}
|
|
}
|
|
|
|
if (!mShadowStyleSpan.offsetMatches(dx, dy)) {
|
|
getShadowSpan().setOffset(dx, dy);
|
|
notifyChanged(false);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = PROP_SHADOW_RADIUS)
|
|
public void setTextShadowRadius(float textShadowRadius) {
|
|
textShadowRadius = PixelUtil.toPixelFromDIP(textShadowRadius);
|
|
if (mShadowStyleSpan.getRadius() != textShadowRadius) {
|
|
getShadowSpan().setRadius(textShadowRadius);
|
|
notifyChanged(false);
|
|
}
|
|
}
|
|
|
|
@ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color")
|
|
public void setTextShadowColor(int textShadowColor) {
|
|
if (mShadowStyleSpan.getColor() != textShadowColor) {
|
|
getShadowSpan().setColor(textShadowColor);
|
|
notifyChanged(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns font size for this node.
|
|
* When called on RCTText, this value is never -1 (unset).
|
|
*/
|
|
protected final int getFontSize() {
|
|
return mFontStylingSpan.getFontSize();
|
|
}
|
|
/**
|
|
* Returns font style for this node.
|
|
*/
|
|
protected final int getFontStyle() {
|
|
int style = mFontStylingSpan.getFontStyle();
|
|
return style >= 0 ? style : Typeface.NORMAL;
|
|
}
|
|
|
|
protected int getDefaultFontSize() {
|
|
return -1;
|
|
}
|
|
|
|
/* package */ static int fontSizeFromSp(float sp) {
|
|
return (int) Math.ceil(PixelUtil.toPixelFromSP(sp));
|
|
}
|
|
|
|
protected final FontStylingSpan getSpan() {
|
|
if (mFontStylingSpan.isFrozen()) {
|
|
mFontStylingSpan = mFontStylingSpan.mutableCopy();
|
|
}
|
|
return mFontStylingSpan;
|
|
}
|
|
|
|
/**
|
|
* Returns a new SpannableStringBuilder that includes all the text and styling information to
|
|
* create the Layout.
|
|
*/
|
|
/* package */ final SpannableStringBuilder getText() {
|
|
SpannableStringBuilder sb = new SpannableStringBuilder();
|
|
collectText(sb);
|
|
applySpans(sb, isEditable());
|
|
return sb;
|
|
}
|
|
|
|
private final ShadowStyleSpan getShadowSpan() {
|
|
if (mShadowStyleSpan.isFrozen()) {
|
|
mShadowStyleSpan = mShadowStyleSpan.mutableCopy();
|
|
}
|
|
return mShadowStyleSpan;
|
|
}
|
|
|
|
/**
|
|
* Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
|
|
* return the weight.
|
|
*/
|
|
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;
|
|
}
|
|
}
|