Add support to measure shadow nodes in the FabricUIManager

Summary: In this diff I added support to be able to measure C++ shadowNode in Android. As an example I implemented the measurement of TextViews

Reviewed By: shergin

Differential Revision: D9583972

fbshipit-source-id: 1344782d4c586c94a4576b18a4acfa4775e46952
This commit is contained in:
David Vacca 2018-09-17 18:46:06 -07:00 committed by Facebook Github Bot
parent 52dd7dbbcf
commit 5c0da011cb
14 changed files with 851 additions and 11 deletions

View File

@ -31,6 +31,13 @@ public class PixelUtil {
return toPixelFromDIP((float) value);
}
/**
* Convert from PX to SP
*/
public static float toSPFromPixel(float value) {
return value / DisplayMetricsHolder.getScreenDisplayMetrics().scaledDensity;
}
/**
* Convert from SP to PX
*/

View File

@ -10,7 +10,9 @@ package com.facebook.react.uimanager;
import android.view.View;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.annotations.ReactProp;
@ -202,4 +204,16 @@ public abstract class ViewManager<T extends View, C extends ReactShadowNode>
public Map<String, String> getNativeProps() {
return ViewManagerPropertyUpdater.getNativeProps(getClass(), getShadowNodeClass());
}
public float[] measure(
ReactContext context,
T view,
ReadableNativeMap localData,
ReadableNativeMap props,
float width,
int widthMode,
float height,
int heightMode) {
return null;
}
}

View File

@ -11,6 +11,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.Gravity;
@ -36,6 +37,7 @@ public class ReactTextView extends TextView implements ReactCompoundView {
private TextUtils.TruncateAt mEllipsizeLocation = TextUtils.TruncateAt.END;
private ReactViewBackgroundManager mReactBackgroundManager;
private Spannable mSpanned;
public ReactTextView(Context context) {
super(context);
@ -255,4 +257,12 @@ public class ReactTextView extends TextView implements ReactCompoundView {
public void setBorderStyle(@Nullable String style) {
mReactBackgroundManager.setBorderStyle(style);
}
public void setSpanned(Spannable spanned) {
mSpanned = spanned;
}
public Spannable getSpanned() {
return mSpanned;
}
}

View File

@ -9,11 +9,14 @@ package com.facebook.react.views.text;
import android.text.Spannable;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import java.util.Map;
import javax.annotation.Nullable;
import com.facebook.yoga.YogaMeasureMode;
/**
* Concrete class for {@link ReactTextAnchorViewManager} which represents view managers of anchor
@ -66,4 +69,25 @@ public class ReactTextViewManager
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of("topTextLayout", MapBuilder.of("registrationName", "onTextLayout"));
}
public float[] measure(
ReactContext context,
ReactTextView view,
ReadableNativeMap localData,
ReadableNativeMap props,
float width,
int widthMode,
float height,
int heightMode) {
// TODO: should widthMode and heightMode be a YogaMeasureMode?
return TextLayoutManager.measureText(context,
view,
localData,
props,
width,
YogaMeasureMode.fromInt(widthMode),
height,
YogaMeasureMode.fromInt(heightMode));
}
}

View File

@ -0,0 +1,416 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.text;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout;
import android.view.Gravity;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.yoga.YogaDirection;
import javax.annotation.Nullable;
public class TextAttributeProps {
private static final String INLINE_IMAGE_PLACEHOLDER = "I";
public static final int UNSET = -1;
private static final String PROP_SHADOW_OFFSET = "textShadowOffset";
private static final String PROP_SHADOW_OFFSET_WIDTH = "width";
private static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
private static final String PROP_SHADOW_RADIUS = "textShadowRadius";
private static final String PROP_SHADOW_COLOR = "textShadowColor";
private static final String PROP_TEXT_TRANSFORM = "textTransform";
private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
protected float mLineHeight = Float.NaN;
protected float mLetterSpacing = Float.NaN;
protected boolean mIsColorSet = false;
protected boolean mAllowFontScaling = true;
protected int mColor;
protected boolean mIsBackgroundColorSet = false;
protected int mBackgroundColor;
protected int mNumberOfLines = UNSET;
protected int mFontSize = UNSET;
protected float mFontSizeInput = UNSET;
protected float mLineHeightInput = UNSET;
protected float mLetterSpacingInput = Float.NaN;
protected int mTextAlign = Gravity.NO_GRAVITY;
protected int mTextBreakStrategy =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
protected TextTransform mTextTransform = TextTransform.UNSET;
protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
protected float mTextShadowRadius = 1;
protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
protected boolean mIsUnderlineTextDecorationSet = false;
protected boolean mIsLineThroughTextDecorationSet = false;
protected boolean mIncludeFontPadding = true;
/**
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
* mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
*/
protected int mFontStyle = UNSET;
protected int mFontWeight = UNSET;
/**
* NB: If a font family is used that does not have a style in a certain Android version (ie.
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
* nodes. To retain that style, you have to add it to those nodes explicitly.
* Example, Android 4.4:
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
*/
protected @Nullable String mFontFamily = null;
protected boolean mContainsImages = false;
protected float mHeightOfTallestInlineImage = Float.NaN;
private final ReactStylesDiffMap mProps;
public TextAttributeProps(ReactStylesDiffMap props) {
mProps = props;
setNumberOfLines(getIntProp(ViewProps.NUMBER_OF_LINES, UNSET));
setLineHeight(getFloatProp(ViewProps.LINE_HEIGHT, UNSET));
setLetterSpacing(getFloatProp(ViewProps.LETTER_SPACING, Float.NaN));
setAllowFontScaling(getBooleanProp(ViewProps.ALLOW_FONT_SCALING, true));
setTextAlign(getStringProp(ViewProps.TEXT_ALIGN));
setFontSize(getFloatProp(ViewProps.FONT_SIZE, UNSET));
setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
setColor(props.hasKey("foregroundColor") ? props.getInt("foregroundColor", 0) : null);
setBackgroundColor(props.hasKey(ViewProps.BACKGROUND_COLOR) ? props.getInt(ViewProps.BACKGROUND_COLOR, 0) : null);
setFontFamily(getStringProp(ViewProps.FONT_FAMILY));
setFontWeight(getStringProp(ViewProps.FONT_WEIGHT));
setFontStyle(getStringProp(ViewProps.FONT_STYLE));
setIncludeFontPadding(getBooleanProp(ViewProps.INCLUDE_FONT_PADDING, true));
setTextDecorationLine(getStringProp(ViewProps.TEXT_DECORATION_LINE));
setTextBreakStrategy(getStringProp(ViewProps.TEXT_BREAK_STRATEGY));
setTextShadowOffset(props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null);
setTextShadowRadius(getIntProp(PROP_SHADOW_RADIUS, 1));
setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR));
setTextTransform(getStringProp(PROP_TEXT_TRANSFORM));
}
private boolean getBooleanProp(String name, boolean defaultValue) {
if (mProps.hasKey(name)) {
return mProps.getBoolean(name, defaultValue);
} else {
return defaultValue;
}
}
private String getStringProp(String name) {
if (mProps.hasKey(name)) {
return mProps.getString(name);
} else {
return null;
}
}
private int getIntProp(String name, int defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getInt(name, defaultvalue);
} else {
return defaultvalue;
}
}
private float getFloatProp(String name, float defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getFloat(name, defaultvalue);
} else {
return defaultvalue;
}
}
// Returns a line height which takes into account the requested line height
// and the height of the inline images.
public float getEffectiveLineHeight() {
boolean useInlineViewHeight =
!Float.isNaN(mLineHeight)
&& !Float.isNaN(mHeightOfTallestInlineImage)
&& mHeightOfTallestInlineImage > mLineHeight;
return useInlineViewHeight ? mHeightOfTallestInlineImage : mLineHeight;
}
// Return text alignment according to LTR or RTL style
public int getTextAlign() {
int textAlign = mTextAlign;
if (getLayoutDirection() == YogaDirection.RTL) {
if (textAlign == Gravity.RIGHT) {
textAlign = Gravity.LEFT;
} else if (textAlign == Gravity.LEFT) {
textAlign = Gravity.RIGHT;
}
}
return textAlign;
}
public void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines;
}
public void setLineHeight(float lineHeight) {
mLineHeightInput = lineHeight;
if (lineHeight == UNSET) {
mLineHeight = Float.NaN;
} else {
mLineHeight =
mAllowFontScaling
? PixelUtil.toPixelFromSP(lineHeight)
: PixelUtil.toPixelFromDIP(lineHeight);
}
}
public void setLetterSpacing(float letterSpacing) {
mLetterSpacingInput = letterSpacing;
mLetterSpacing = mAllowFontScaling
? PixelUtil.toPixelFromSP(mLetterSpacingInput)
: PixelUtil.toPixelFromDIP(mLetterSpacingInput);
}
public void setAllowFontScaling(boolean allowFontScaling) {
if (allowFontScaling != mAllowFontScaling) {
mAllowFontScaling = allowFontScaling;
setFontSize(mFontSizeInput);
setLineHeight(mLineHeightInput);
setLetterSpacing(mLetterSpacingInput);
}
}
public void setTextAlign(@Nullable String textAlign) {
if (textAlign == null || "auto".equals(textAlign)) {
mTextAlign = Gravity.NO_GRAVITY;
} else if ("left".equals(textAlign)) {
mTextAlign = Gravity.LEFT;
} else if ("right".equals(textAlign)) {
mTextAlign = Gravity.RIGHT;
} else if ("center".equals(textAlign)) {
mTextAlign = Gravity.CENTER_HORIZONTAL;
} else if ("justify".equals(textAlign)) {
// Fallback gracefully for cross-platform compat instead of error
mTextAlign = Gravity.LEFT;
} else {
throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign);
}
}
public void setFontSize(float fontSize) {
mFontSizeInput = fontSize;
if (fontSize != UNSET) {
fontSize =
mAllowFontScaling
? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
: (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
}
mFontSize = (int) fontSize;
}
public void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
if (mIsColorSet) {
mColor = color;
}
}
public void setBackgroundColor(Integer color) {
//TODO: Don't apply background color to anchor TextView since it will be applied on the View directly
//if (!isVirtualAnchor()) {
mIsBackgroundColorSet = (color != null);
if (mIsBackgroundColorSet) {
mBackgroundColor = color;
}
//}
}
public void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
}
/**
/* This code is duplicated in ReactTextInputManager
/* TODO: Factor into a common place they can both use
*/
public void setFontWeight(@Nullable String fontWeightString) {
int fontWeightNumeric =
fontWeightString != null ? parseNumericFontWeight(fontWeightString) : -1;
int fontWeight = UNSET;
if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) {
fontWeight = Typeface.BOLD;
} else if ("normal".equals(fontWeightString)
|| (fontWeightNumeric != -1 && fontWeightNumeric < 500)) {
fontWeight = Typeface.NORMAL;
}
if (fontWeight != mFontWeight) {
mFontWeight = fontWeight;
}
}
/**
/* This code is duplicated in ReactTextInputManager
/* TODO: Factor into a common place they can both use
*/
public void setFontStyle(@Nullable String fontStyleString) {
int fontStyle = UNSET;
if ("italic".equals(fontStyleString)) {
fontStyle = Typeface.ITALIC;
} else if ("normal".equals(fontStyleString)) {
fontStyle = Typeface.NORMAL;
}
if (fontStyle != mFontStyle) {
mFontStyle = fontStyle;
}
}
public void setIncludeFontPadding(boolean includepad) {
mIncludeFontPadding = includepad;
}
public void setTextDecorationLine(@Nullable String textDecorationLineString) {
mIsUnderlineTextDecorationSet = false;
mIsLineThroughTextDecorationSet = false;
if (textDecorationLineString != null) {
for (String textDecorationLineSubString : textDecorationLineString.split(" ")) {
if ("underline".equals(textDecorationLineSubString)) {
mIsUnderlineTextDecorationSet = true;
} else if ("line-through".equals(textDecorationLineSubString)) {
mIsLineThroughTextDecorationSet = true;
}
}
}
}
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
if (textBreakStrategy == null || "highQuality".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
} else if ("simple".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
} else if ("balanced".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
} else {
throw new JSApplicationIllegalArgumentException(
"Invalid textBreakStrategy: " + textBreakStrategy);
}
}
public void setTextShadowOffset(ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
mTextShadowOffsetDy = 0;
if (offsetMap != null) {
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
mTextShadowOffsetDx =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH));
}
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
mTextShadowOffsetDy =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT));
}
}
}
public void setTextShadowRadius(float textShadowRadius) {
if (textShadowRadius != mTextShadowRadius) {
mTextShadowRadius = textShadowRadius;
}
}
public void setTextShadowColor(int textShadowColor) {
if (textShadowColor != mTextShadowColor) {
mTextShadowColor = textShadowColor;
}
}
public void setTextTransform(@Nullable String textTransform) {
if (textTransform == null || "none".equals(textTransform)) {
mTextTransform = TextTransform.NONE;
} else if ("uppercase".equals(textTransform)) {
mTextTransform = TextTransform.UPPERCASE;
} else if ("lowercase".equals(textTransform)) {
mTextTransform = TextTransform.LOWERCASE;
} else if ("capitalize".equals(textTransform)) {
mTextTransform = TextTransform.CAPITALIZE;
} else {
throw new JSApplicationIllegalArgumentException("Invalid textTransform: " + textTransform);
}
}
/**
* Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
* return the weight.
*
* This code is duplicated in ReactTextInputManager
* TODO: Factor into a common place they can both use
*/
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;
}
//TODO remove this from here
private YogaDirection getLayoutDirection() {
return YogaDirection.LTR;
}
public float getBottomPadding() {
// TODO convert into constants
return getFloatProp("bottomPadding", 0f);
}
public float getLeftPadding() {
return getFloatProp("leftPadding", 0f);
}
public float getStartPadding() {
return getFloatProp("startPadding", 0f);
}
public float getEndPadding() {
return getFloatProp("endPadding", 0f);
}
public float getTopPadding() {
return getFloatProp("topPadding", 0f);
}
public float getRightPadding() {
return getFloatProp("rightPadding", 0f);
}
}

View File

@ -0,0 +1,337 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.text;
import static com.facebook.react.views.text.TextAttributeProps.UNSET;
import android.content.Context;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import java.util.ArrayList;
import java.util.List;
/**
* Class responsible of creating {@link Spanned} object for the JS representation of Text
*/
public class TextLayoutManager {
// It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it
// later by calling setFlags. This is because the latter approach triggers a bug on Android 4.4.2.
// The bug is that unicode emoticons aren't measured properly which causes text to be clipped.
private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
private static void buildSpannedFromShadowNode(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
for (int i = 0, length = fragments.size(); i < length; i++) {
ReadableMap fragment = fragments.getMap(i);
int start = sb.length();
//ReactRawText
sb.append(fragment.getString("string"));
// TODO: add support for TextInlineImage and BaseText
// if (child instanceof ReactRawTextShadowNode) {
// sb.append(((ReactRawTextShadowNode) child).getText());
// } else if (child instanceof ReactBaseTextShadowNode) {
// buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops);
// } else if (child instanceof ReactTextInlineImageShadowNode) {
// // We make the image take up 1 character in the span and put a corresponding character into
// // the text so that the image doesn't run over any following text.
// sb.append(INLINE_IMAGE_PLACEHOLDER);
// ops.add(
// new SetSpanOperation(
// sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
// sb.length(),
// ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
// } else {
// throw new IllegalViewOperationException(
// "Unexpected view type nested under text node: " + child.getClass());
// }
TextAttributeProps textAttributes = new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
int end = sb.length();
if (end >= start) {
if (textAttributes.mIsColorSet) {
ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textAttributes.mColor)));
}
if (textAttributes.mIsBackgroundColorSet) {
ops.add(
new SetSpanOperation(
start, end, new BackgroundColorSpan(textAttributes.mBackgroundColor)));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (!Float.isNaN(textAttributes.mLetterSpacing)) {
ops.add(new SetSpanOperation(
start,
end,
new CustomLetterSpacingSpan(textAttributes.mLetterSpacing)));
}
}
if (textAttributes.mFontSize != UNSET) {
ops.add(
new SetSpanOperation(
start, end, new AbsoluteSizeSpan((int) (textAttributes.mFontSize))));
}
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomStyleSpan(
textAttributes.mFontStyle,
textAttributes.mFontWeight,
textAttributes.mFontFamily,
context.getAssets())));
}
if (textAttributes.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new UnderlineSpan()));
}
if (textAttributes.mIsLineThroughTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new StrikethroughSpan()));
}
if (textAttributes.mTextShadowOffsetDx != 0 || textAttributes.mTextShadowOffsetDy != 0) {
ops.add(
new SetSpanOperation(
start,
end,
new ShadowStyleSpan(
textAttributes.mTextShadowOffsetDx,
textAttributes.mTextShadowOffsetDy,
textAttributes.mTextShadowRadius,
textAttributes.mTextShadowColor)));
}
if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) {
ops.add(
new SetSpanOperation(
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
}
if (textAttributes.mTextTransform != TextTransform.UNSET && textAttributes.mTextTransform != TextTransform.NONE) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomTextTransformSpan(textAttributes.mTextTransform)));
}
//TODO: add react tag as part of the fragments, react tag is used on Touch events
int reactTag = 1;
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
}
}
}
protected static Spannable spannedFromTextFragments(
Context context,
ReadableArray fragments, String text) {
SpannableStringBuilder sb = new SpannableStringBuilder();
// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
// up-to-bottom, otherwise all the spannables that are withing the region for which one may set
// a new spannable will be wiped out
List<SetSpanOperation> ops = new ArrayList<>();
buildSpannedFromShadowNode(context, fragments, sb, ops);
// TODO: add support for AllowScaling in C++
// if (textShadowNode.mFontSize == UNSET) {
// int defaultFontSize =
// textShadowNode.mAllowFontScaling
// ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
// : (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
//
// ops.add(new SetSpanOperation(0, sb.length(), new AbsoluteSizeSpan(defaultFontSize)));
// }
//
// textShadowNode.mContainsImages = false;
// textShadowNode.mHeightOfTallestInlineImage = Float.NaN;
// While setting the Spans on the final text, we also check whether any of them are images.
int priority = 0;
for (SetSpanOperation op : ops) {
// TODO: add support for TextInlineImage in C++
// if (op.what instanceof TextInlineImageSpan) {
// int height = ((TextInlineImageSpan) op.what).getHeight();
// textShadowNode.mContainsImages = true;
// if (Float.isNaN(textShadowNode.mHeightOfTallestInlineImage)
// || height > textShadowNode.mHeightOfTallestInlineImage) {
// textShadowNode.mHeightOfTallestInlineImage = height;
// }
// }
// Actual order of calling {@code execute} does NOT matter,
// but the {@code priority} DOES matter.
op.execute(sb, priority);
priority++;
}
return sb;
}
public static float[] measureText(
ReactContext context,
ReactTextView view,
ReadableNativeMap attributedString,
ReadableNativeMap paragraphAttributes,
float width,
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode) {
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
TextPaint textPaint = sTextPaintInstance;
Layout layout;
Spannable preparedSpannableText = view == null ? null : view.getSpanned();
// TODO add these props to paragraph attributes
int textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
boolean includeFontPadding = true;
if (preparedSpannableText == null) {
preparedSpannableText = spannedFromTextFragments(context, attributedString.getArray("fragments"), attributedString.getString("string"));
}
if (preparedSpannableText == null) {
throw new IllegalStateException("Spannable element has not been prepared in onBeforeLayout");
}
Spanned text = preparedSpannableText;
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
float desiredWidth = boring == null ?
Layout.getDesiredWidth(text, textPaint) : Float.NaN;
// technically, width should never be negative, but there is currently a bug in
boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0;
if (boring == null &&
(unconstrainedWidth ||
(!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
// Is used when the width is not known and the text is not boring, ie. if it contains
// unicode characters.
int hintWidth = (int) Math.ceil(desiredWidth);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
layout = new StaticLayout(
text,
textPaint,
hintWidth,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
includeFontPadding);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.build();
}
} else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
// Is used for single-line, boring text when the width is either unknown or bigger
// than the width of the text.
layout = BoringLayout.make(
text,
textPaint,
boring.width,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
boring,
includeFontPadding);
} else {
// Is used for multiline, boring text and the width is known.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
layout = new StaticLayout(
text,
textPaint,
(int) width,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
includeFontPadding);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.build();
}
}
int maximumNumberOfLines = paragraphAttributes.hasKey("maximumNumberOfLines") ? paragraphAttributes.getInt("maximumNumberOfLines") : UNSET;
width = layout.getWidth();
if (maximumNumberOfLines != UNSET
&& maximumNumberOfLines != 0
&& maximumNumberOfLines < layout.getLineCount()) {
height = layout.getLineBottom(maximumNumberOfLines - 1);
} else {
height = layout.getHeight();
}
return new float[] { PixelUtil.toSPFromPixel(width), PixelUtil.toSPFromPixel(height) };
}
private static class SetSpanOperation {
protected int start, end;
protected Object what;
SetSpanOperation(int start, int end, Object what) {
this.start = start;
this.end = end;
this.what = what;
}
public void execute(SpannableStringBuilder sb, int priority) {
// 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.
int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
if (start == 0) {
spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
}
spanFlags &= ~Spannable.SPAN_PRIORITY;
spanFlags |= (priority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY;
sb.setSpan(what, start, end, spanFlags);
}
}
}

View File

@ -67,6 +67,7 @@ rn_xplat_cxx_library(
react_native_xplat_target("fabric/graphics:graphics"),
react_native_xplat_target("fabric/textlayoutmanager:textlayoutmanager"),
react_native_xplat_target("fabric/components/view:view"),
react_native_xplat_target("fabric/uimanager:uimanager"),
],
)

View File

@ -10,6 +10,7 @@
#include <fabric/components/text/ParagraphShadowNode.h>
#include <fabric/core/ConcreteComponentDescriptor.h>
#include <fabric/textlayoutmanager/TextLayoutManager.h>
#include <fabric/uimanager/ContextContainer.h>
namespace facebook {
namespace react {
@ -22,11 +23,11 @@ class ParagraphComponentDescriptor final:
public:
ParagraphComponentDescriptor(SharedEventDispatcher eventDispatcher):
ParagraphComponentDescriptor(SharedEventDispatcher eventDispatcher, const SharedContextContainer &contextContainer):
ConcreteComponentDescriptor<ParagraphShadowNode>(eventDispatcher) {
// Every single `ParagraphShadowNode` will have a reference to
// a shared `TextLayoutManager`.
textLayoutManager_ = std::make_shared<TextLayoutManager>();
textLayoutManager_ = std::make_shared<TextLayoutManager>(contextContainer);
}
void adopt(UnsharedShadowNode shadowNode) const override {

View File

@ -41,6 +41,7 @@ void ParagraphShadowNode::updateLocalData() {
Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const {
return textLayoutManager_->measure(
getTag(),
getAttributedString(),
getProps()->paragraphAttributes,
layoutConstraints

View File

@ -7,6 +7,7 @@ load(
"get_apple_compiler_flags",
"get_apple_inspector_flags",
"react_native_xplat_target",
"react_native_target",
"rn_xplat_cxx_library",
"subdir_glob",
)
@ -39,6 +40,9 @@ rn_xplat_cxx_library(
"-std=c++14",
"-Wall",
],
fbandroid_deps = [
react_native_target("jni/react/jni:jni"),
],
fbandroid_exported_headers = subdir_glob(
[
("", "*.h"),
@ -107,6 +111,7 @@ rn_xplat_cxx_library(
react_native_xplat_target("fabric/core:core"),
react_native_xplat_target("fabric/debug:debug"),
react_native_xplat_target("fabric/graphics:graphics"),
react_native_xplat_target("fabric/uimanager:uimanager"),
],
)

View File

@ -7,12 +7,13 @@
#include "TextLayoutManager.h"
#include <react/jni/ReadableNativeMap.h>
using namespace facebook::jni;
namespace facebook {
namespace react {
TextLayoutManager::TextLayoutManager() {
}
TextLayoutManager::~TextLayoutManager() {
}
@ -21,12 +22,28 @@ void *TextLayoutManager::getNativeTextLayoutManager() const {
}
Size TextLayoutManager::measure(
Tag reactTag,
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints
) const {
// Not implemented.
return {};
const jni::global_ref<jobject> & fabricUIManager = contextContainer_->getInstance<jni::global_ref<jobject>>("FabricUIManager");
auto clazz = jni::findClassStatic("com/facebook/fbreact/fabricxx/UIManager");
static auto measure =
clazz->getMethod<JArrayFloat::javaobject(jint, jstring, ReadableNativeMap::javaobject, ReadableNativeMap::javaobject, jint, jint)>("measure");
int width = (int) layoutConstraints.maximumSize.width;
int height = (int) layoutConstraints.maximumSize.height;
local_ref<JString> componentName = make_jstring("RCTText");
auto values = measure(fabricUIManager, reactTag, componentName.get(), ReadableNativeMap::newObjectCxxArgs(attributedString.toDynamic()).get(), ReadableNativeMap::newObjectCxxArgs(paragraphAttributes.toDynamic()).get(), width, height);
std::vector<float> indices;
indices.resize(values->size());
values->getRegion(0, values->size(), indices.data());
return {(float) indices[0], (float) indices[1]};
}
} // namespace react

View File

@ -12,6 +12,7 @@
#include <fabric/attributedstring/AttributedString.h>
#include <fabric/attributedstring/ParagraphAttributes.h>
#include <fabric/core/LayoutConstraints.h>
#include <fabric/uimanager/ContextContainer.h>
namespace facebook {
namespace react {
@ -27,13 +28,14 @@ class TextLayoutManager {
public:
TextLayoutManager();
TextLayoutManager(const SharedContextContainer &contextContainer) : contextContainer_(contextContainer) {};
~TextLayoutManager();
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
Size measure(
Tag reactTag,
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints
@ -46,8 +48,10 @@ public:
void *getNativeTextLayoutManager() const;
private:
void *self_;
SharedContextContainer contextContainer_;
};
} // namespace react

View File

@ -12,6 +12,7 @@
#include <fabric/attributedstring/AttributedString.h>
#include <fabric/attributedstring/ParagraphAttributes.h>
#include <fabric/core/LayoutConstraints.h>
#include <fabric/uimanager/ContextContainer.h>
namespace facebook {
namespace react {
@ -25,13 +26,14 @@ using SharedTextLayoutManager = std::shared_ptr<const TextLayoutManager>;
*/
class TextLayoutManager {
public:
TextLayoutManager();
TextLayoutManager(const SharedContextContainer &contextContainer);
~TextLayoutManager();
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
Size measure(
Tag reactTag,
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints

View File

@ -12,7 +12,7 @@
namespace facebook {
namespace react {
TextLayoutManager::TextLayoutManager() {
TextLayoutManager::TextLayoutManager(const SharedContextContainer &contextContainer) {
self_ = (__bridge_retained void *)[RCTTextLayoutManager new];
}
@ -26,6 +26,7 @@ void *TextLayoutManager::getNativeTextLayoutManager() const {
}
Size TextLayoutManager::measure(
Tag reactTag,
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints