mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
5f3f9caceb
Summary: Made some improvements to RCTText based on some of our learnings from components for android. This now resembles diffusion/FBS/browse/master/fbandroid/java/com/facebook/components/widget/TextSpec.java Things that have improved: - Calculation of text width is now faster (we noticed in components that .getWith() on the layout is all that is needed and it is much faster) - Use text layout builder to abstract away a lot of the low level details of static / boring layouts and text measurements - Handle MeasureMode correctly, previously AT_MOST was not supported. - Better handling of RTL text by using TextLayoutBuilder where I made changes to support RTL text in components. Specifically RTL text measured with UNSPECIFIED or AT_MOST. - There was an incorrect assumption being made that when measure() was not called the text had to be boring. This is incorrect, Arabic text is never boring for example. Also multiline text is not boring either and may have exact sizing. Reviewed By: ahmedre Differential Revision: D3374752
284 lines
9.0 KiB
Java
284 lines
9.0 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) {
|
|
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 = 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);
|
|
}
|
|
}
|
|
|
|
@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.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);
|
|
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;
|
|
}
|
|
}
|