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
318 lines
9.1 KiB
Java
318 lines
9.1 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.support.v4.text.TextDirectionHeuristicsCompat;
|
|
import android.text.Layout;
|
|
import android.text.TextUtils;
|
|
|
|
import com.facebook.csslayout.CSSMeasureMode;
|
|
import com.facebook.csslayout.CSSNode;
|
|
import com.facebook.csslayout.MeasureOutput;
|
|
import com.facebook.csslayout.Spacing;
|
|
import com.facebook.fbui.widget.text.layoutbuilder.TextLayoutBuilder;
|
|
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
|
import com.facebook.react.uimanager.PixelUtil;
|
|
import com.facebook.react.uimanager.ViewDefaults;
|
|
import com.facebook.react.uimanager.ViewProps;
|
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
|
|
/**
|
|
* RCTText is a top-level node for text. It extends {@link RCTVirtualText} because it can contain
|
|
* styling information, but has the following differences:
|
|
*
|
|
* a) RCTText is not a virtual node, and can be measured and laid out.
|
|
* b) when no font size is specified, a font size of ViewDefaults#FONT_SIZE_SP is assumed.
|
|
*/
|
|
/* package */ final class RCTText extends RCTVirtualText implements CSSNode.MeasureFunction {
|
|
|
|
private static final TextLayoutBuilder sTextLayoutBuilder =
|
|
new TextLayoutBuilder().setShouldCacheLayout(false);
|
|
|
|
private @Nullable CharSequence mText;
|
|
private @Nullable DrawTextLayout mDrawCommand;
|
|
private float mSpacingMult = 1.0f;
|
|
private float mSpacingAdd = 0.0f;
|
|
private int mNumberOfLines = Integer.MAX_VALUE;
|
|
private Layout.Alignment mAlignment = Layout.Alignment.ALIGN_NORMAL;
|
|
|
|
public RCTText() {
|
|
setMeasureFunction(this);
|
|
getSpan().setFontSize(getDefaultFontSize());
|
|
}
|
|
|
|
@Override
|
|
public boolean isVirtual() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean isVirtualAnchor() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void measure(
|
|
CSSNode node,
|
|
float width,
|
|
CSSMeasureMode widthMode,
|
|
float height,
|
|
CSSMeasureMode heightMode,
|
|
MeasureOutput measureOutput) {
|
|
|
|
CharSequence text = getText();
|
|
if (TextUtils.isEmpty(text)) {
|
|
// to indicate that we don't have anything to display
|
|
mText = null;
|
|
measureOutput.width = 0;
|
|
measureOutput.height = 0;
|
|
return;
|
|
} else {
|
|
mText = text;
|
|
}
|
|
|
|
Layout layout = createTextLayout(
|
|
(int) Math.ceil(width),
|
|
widthMode,
|
|
TextUtils.TruncateAt.END,
|
|
true,
|
|
mNumberOfLines,
|
|
mNumberOfLines == 1,
|
|
text,
|
|
getFontSize(),
|
|
mSpacingAdd,
|
|
mSpacingMult,
|
|
getFontStyle(),
|
|
mAlignment);
|
|
|
|
if (mDrawCommand != null && !mDrawCommand.isFrozen()) {
|
|
mDrawCommand.setLayout(layout);
|
|
} else {
|
|
mDrawCommand = new DrawTextLayout(layout);
|
|
}
|
|
|
|
measureOutput.width = mDrawCommand.getLayoutWidth();
|
|
measureOutput.height = mDrawCommand.getLayoutHeight();
|
|
}
|
|
|
|
@Override
|
|
protected void collectState(
|
|
StateBuilder stateBuilder,
|
|
float left,
|
|
float top,
|
|
float right,
|
|
float bottom,
|
|
float clipLeft,
|
|
float clipTop,
|
|
float clipRight,
|
|
float clipBottom) {
|
|
|
|
super.collectState(
|
|
stateBuilder,
|
|
left,
|
|
top,
|
|
right,
|
|
bottom,
|
|
clipLeft,
|
|
clipTop,
|
|
clipRight,
|
|
clipBottom);
|
|
|
|
if (mText == null) {
|
|
// nothing to draw (empty text).
|
|
return;
|
|
}
|
|
|
|
boolean updateNodeRegion = false;
|
|
if (mDrawCommand == null) {
|
|
mDrawCommand = new DrawTextLayout(createTextLayout(
|
|
(int) Math.ceil(right - left),
|
|
CSSMeasureMode.EXACTLY,
|
|
TextUtils.TruncateAt.END,
|
|
true,
|
|
mNumberOfLines,
|
|
mNumberOfLines == 1,
|
|
mText,
|
|
getFontSize(),
|
|
mSpacingAdd,
|
|
mSpacingMult,
|
|
getFontStyle(),
|
|
mAlignment));
|
|
updateNodeRegion = true;
|
|
}
|
|
|
|
Spacing padding = getPadding();
|
|
|
|
left += padding.get(Spacing.LEFT);
|
|
top += padding.get(Spacing.TOP);
|
|
|
|
// these are actual right/bottom coordinates where this DrawCommand will draw.
|
|
right = left + mDrawCommand.getLayoutWidth();
|
|
bottom = top + mDrawCommand.getLayoutHeight();
|
|
|
|
mDrawCommand = (DrawTextLayout) mDrawCommand.updateBoundsAndFreeze(
|
|
left,
|
|
top,
|
|
right,
|
|
bottom,
|
|
clipLeft,
|
|
clipTop,
|
|
clipRight,
|
|
clipBottom);
|
|
stateBuilder.addDrawCommand(mDrawCommand);
|
|
|
|
if (updateNodeRegion) {
|
|
NodeRegion nodeRegion = getNodeRegion();
|
|
if (nodeRegion instanceof TextNodeRegion) {
|
|
((TextNodeRegion) nodeRegion).setLayout(mDrawCommand.getLayout());
|
|
}
|
|
}
|
|
|
|
performCollectAttachDetachListeners(stateBuilder);
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.LINE_HEIGHT, defaultDouble = Double.NaN)
|
|
public void setLineHeight(double lineHeight) {
|
|
if (Double.isNaN(lineHeight)) {
|
|
mSpacingMult = 1.0f;
|
|
mSpacingAdd = 0.0f;
|
|
} else {
|
|
mSpacingMult = 0.0f;
|
|
mSpacingAdd = PixelUtil.toPixelFromSP((float) lineHeight);
|
|
}
|
|
notifyChanged(true);
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = Integer.MAX_VALUE)
|
|
public void setNumberOfLines(int numberOfLines) {
|
|
mNumberOfLines = numberOfLines;
|
|
notifyChanged(true);
|
|
}
|
|
|
|
@Override
|
|
/* package */ void updateNodeRegion(
|
|
float left,
|
|
float top,
|
|
float right,
|
|
float bottom,
|
|
boolean isVirtual) {
|
|
|
|
NodeRegion nodeRegion = getNodeRegion();
|
|
if (mDrawCommand == null) {
|
|
if (nodeRegion.mLeft != left || nodeRegion.mTop != top || nodeRegion.mRight != right ||
|
|
nodeRegion.mBottom != bottom || nodeRegion.mIsVirtual != isVirtual) {
|
|
setNodeRegion(new TextNodeRegion(left, top, right, bottom, getReactTag(), isVirtual, null));
|
|
}
|
|
return;
|
|
}
|
|
|
|
Layout layout = null;
|
|
|
|
if (nodeRegion instanceof TextNodeRegion) {
|
|
layout = ((TextNodeRegion) nodeRegion).getLayout();
|
|
}
|
|
|
|
Layout newLayout = mDrawCommand.getLayout();
|
|
if (nodeRegion.mLeft != left || nodeRegion.mTop != top ||
|
|
nodeRegion.mRight != right || nodeRegion.mBottom != bottom ||
|
|
nodeRegion.mIsVirtual != isVirtual || layout != newLayout) {
|
|
setNodeRegion(
|
|
new TextNodeRegion(left, top, right, bottom, getReactTag(), isVirtual, newLayout));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected int getDefaultFontSize() {
|
|
// top-level <Text /> should always specify font size.
|
|
return fontSizeFromSp(ViewDefaults.FONT_SIZE_SP);
|
|
}
|
|
|
|
@Override
|
|
protected void notifyChanged(boolean shouldRemeasure) {
|
|
// Future patch: should only recreate Layout if shouldRemeasure is false
|
|
dirty();
|
|
}
|
|
|
|
@ReactProp(name = ViewProps.TEXT_ALIGN)
|
|
public void setTextAlign(@Nullable String textAlign) {
|
|
if (textAlign == null || "auto".equals(textAlign)) {
|
|
mAlignment = Layout.Alignment.ALIGN_NORMAL;
|
|
} else if ("left".equals(textAlign)) {
|
|
// left and right may yield potentially different results (relative to non-nodes) in cases
|
|
// when supportsRTL="true" in the manifest.
|
|
mAlignment = Layout.Alignment.ALIGN_NORMAL;
|
|
} else if ("right".equals(textAlign)) {
|
|
mAlignment = Layout.Alignment.ALIGN_OPPOSITE;
|
|
} else if ("center".equals(textAlign)) {
|
|
mAlignment = Layout.Alignment.ALIGN_CENTER;
|
|
} else {
|
|
throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
|
|
}
|
|
notifyChanged(false);
|
|
}
|
|
|
|
private static Layout createTextLayout(
|
|
int width,
|
|
CSSMeasureMode widthMode,
|
|
TextUtils.TruncateAt ellipsize,
|
|
boolean shouldIncludeFontPadding,
|
|
int maxLines,
|
|
boolean isSingleLine,
|
|
CharSequence text,
|
|
int textSize,
|
|
float extraSpacing,
|
|
float spacingMultiplier,
|
|
int textStyle,
|
|
Layout.Alignment textAlignment) {
|
|
Layout newLayout;
|
|
|
|
TextLayoutBuilder layoutBuilder = sTextLayoutBuilder;
|
|
|
|
final @TextLayoutBuilder.MeasureMode int textMeasureMode;
|
|
switch (widthMode) {
|
|
case UNDEFINED:
|
|
textMeasureMode = TextLayoutBuilder.MEASURE_MODE_UNSPECIFIED;
|
|
break;
|
|
case EXACTLY:
|
|
textMeasureMode = TextLayoutBuilder.MEASURE_MODE_EXACTLY;
|
|
break;
|
|
case AT_MOST:
|
|
textMeasureMode = TextLayoutBuilder.MEASURE_MODE_AT_MOST;
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unexpected size mode: " + widthMode);
|
|
}
|
|
|
|
layoutBuilder
|
|
.setEllipsize(ellipsize)
|
|
.setMaxLines(maxLines)
|
|
.setSingleLine(isSingleLine)
|
|
.setText(text)
|
|
.setTextSize(textSize)
|
|
.setWidth(width, textMeasureMode);
|
|
|
|
layoutBuilder.setTextStyle(textStyle);
|
|
|
|
layoutBuilder.textDirection(TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR);
|
|
layoutBuilder.setIncludeFontPadding(shouldIncludeFontPadding);
|
|
layoutBuilder.setTextSpacingExtra(extraSpacing);
|
|
layoutBuilder.setTextSpacingMultiplier(spacingMultiplier);
|
|
layoutBuilder.setAlignment(textAlignment);
|
|
|
|
newLayout = layoutBuilder.build();
|
|
|
|
layoutBuilder.setText(null);
|
|
|
|
return newLayout;
|
|
}
|
|
}
|