Files
react-native/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java
T
Emil Sjolander 242f5e9198 BREAKING - Change measure() api to remove need for MeasureOutput allocation
Summary: This is an API breaking change done to allow us to avoid an allocation during measurement. Instead we do the same trick as is done when passing measure results to C, we path them into a long.

Reviewed By: splhack

Differential Revision: D4081037
2016-12-19 13:40:35 -08:00

349 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.support.v4.text.TextDirectionHeuristicsCompat;
import android.text.Layout;
import android.text.TextUtils;
import android.view.Gravity;
import com.facebook.csslayout.CSSDirection;
import com.facebook.csslayout.CSSMeasureMode;
import com.facebook.csslayout.CSSNodeAPI;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.csslayout.Spacing;
import com.facebook.fbui.textlayoutbuilder.TextLayoutBuilder;
import com.facebook.fbui.textlayoutbuilder.glyphwarmer.GlyphWarmerImpl;
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 CSSNodeAPI.MeasureFunction {
// index of left and right in the Layout.Alignment enum since the base values are @hide
private static final int ALIGNMENT_LEFT = 3;
private static final int ALIGNMENT_RIGHT = 4;
// We set every value we use every time we use the layout builder, so we can get away with only
// using a single instance.
private static final TextLayoutBuilder sTextLayoutBuilder =
new TextLayoutBuilder()
.setShouldCacheLayout(false)
.setShouldWarmText(true)
.setGlyphWarmer(new GlyphWarmerImpl());
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 int mAlignment = Gravity.NO_GRAVITY;
public RCTText() {
setMeasureFunction(this);
getSpan().setFontSize(getDefaultFontSize());
}
@Override
public boolean isVirtual() {
return false;
}
@Override
public boolean isVirtualAnchor() {
return true;
}
@Override
public long measure(
CSSNodeAPI node,
float width,
CSSMeasureMode widthMode,
float height,
CSSMeasureMode heightMode) {
CharSequence text = getText();
if (TextUtils.isEmpty(text)) {
// to indicate that we don't have anything to display
mText = null;
return MeasureOutput.make(0, 0);
} else {
mText = text;
}
Layout layout = createTextLayout(
(int) Math.ceil(width),
widthMode,
TextUtils.TruncateAt.END,
true,
mNumberOfLines,
mNumberOfLines == 1,
text,
getFontSize(),
mSpacingAdd,
mSpacingMult,
getFontStyle(),
getAlignment());
if (mDrawCommand != null && !mDrawCommand.isFrozen()) {
mDrawCommand.setLayout(layout);
} else {
mDrawCommand = new DrawTextLayout(layout);
}
return MeasureOutput.make(mDrawCommand.getLayoutWidth(), 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) {
// as an optimization, LayoutEngine may not call measure in certain cases, such as when the
// dimensions are already defined. in these cases, we should still draw the text.
if (bottom - top > 0 && right - left > 0) {
CharSequence text = getText();
if (!TextUtils.isEmpty(text)) {
mText = text;
}
}
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(),
getAlignment()));
updateNodeRegion = true;
}
left += getPadding(Spacing.LEFT);
top += getPadding(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.matches(left, top, right, bottom, 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.matches(left, top, right, bottom, 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 = Gravity.NO_GRAVITY;
} 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 = Gravity.LEFT;
} else if ("right".equals(textAlign)) {
mAlignment = Gravity.RIGHT;
} else if ("center".equals(textAlign)) {
mAlignment = Gravity.CENTER;
} else {
throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
}
notifyChanged(false);
}
public Layout.Alignment getAlignment() {
boolean isRtl = getLayoutDirection() == CSSDirection.RTL;
switch (mAlignment) {
// Layout.Alignment.RIGHT and Layout.Alignment.LEFT are @hide :(
case Gravity.LEFT:
int index = isRtl ? ALIGNMENT_RIGHT : ALIGNMENT_LEFT;
return Layout.Alignment.values()[index];
case Gravity.RIGHT:
index = isRtl ? ALIGNMENT_LEFT : ALIGNMENT_RIGHT;
return Layout.Alignment.values()[index];
case Gravity.CENTER:
return Layout.Alignment.ALIGN_CENTER;
case Gravity.NO_GRAVITY:
default:
return Layout.Alignment.ALIGN_NORMAL;
}
}
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;
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);
}
sTextLayoutBuilder
.setEllipsize(ellipsize)
.setMaxLines(maxLines)
.setSingleLine(isSingleLine)
.setText(text)
.setTextSize(textSize)
.setWidth(width, textMeasureMode);
sTextLayoutBuilder.setTextStyle(textStyle);
sTextLayoutBuilder.setTextDirection(TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR);
sTextLayoutBuilder.setIncludeFontPadding(shouldIncludeFontPadding);
sTextLayoutBuilder.setTextSpacingExtra(extraSpacing);
sTextLayoutBuilder.setTextSpacingMultiplier(spacingMultiplier);
sTextLayoutBuilder.setAlignment(textAlignment);
newLayout = sTextLayoutBuilder.build();
sTextLayoutBuilder.setText(null);
return newLayout;
}
}