/** * 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; } }