/**
* 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.yoga.YogaDirection;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaNodeAPI;
import com.facebook.yoga.YogaMeasureOutput;
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.Spacing;
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 YogaMeasureFunction {
// 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(
YogaNodeAPI node,
float width,
YogaMeasureMode widthMode,
float height,
YogaMeasureMode heightMode) {
CharSequence text = getText();
if (TextUtils.isEmpty(text)) {
// to indicate that we don't have anything to display
mText = null;
return YogaMeasureOutput.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 YogaMeasureOutput.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),
YogaMeasureMode.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);
}
@Override
boolean doesDraw() {
// assume text always draws - this is a performance optimization to avoid having to
// getText() when layout was skipped and when collectState wasn't yet called
return true;
}
@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() == YogaDirection.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,
YogaMeasureMode 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;
}
}