fbshipit-source-id: da15f69185e724eaf7d4bc78dbc61fcdcb3074d5

This commit is contained in:
Héctor Ramos
2020-03-13 21:46:45 -07:00
parent 10c39847a7
commit 07def55396
18 changed files with 554 additions and 389 deletions
@@ -10,7 +10,6 @@ rn_android_library(
react_native_dep("third-party/java/jsr-330:jsr-330"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
":FBReactNativeSpec-jni",
],
exported_deps = [
react_native_target("java/com/facebook/react/turbomodule/core/interfaces:interfaces"),
@@ -800,8 +800,8 @@ public class UIViewOperationQueue {
long runStartTime = SystemClock.uptimeMillis();
// All ViewCommands should be executed first as a perf optimization
if (mViewCommandOperations != null) {
for (UIOperation viewCommandOp : mViewCommandOperations) {
if (viewCommandOperations != null) {
for (UIOperation viewCommandOp : viewCommandOperations) {
viewCommandOp.execute();
}
}
@@ -8,7 +8,6 @@
package com.facebook.react.views.text;
import android.content.Context;
import android.text.Layout;
import android.text.Spannable;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
@@ -95,10 +94,7 @@ public class ReactTextViewManager
view.setSpanned(spanned);
int textBreakStrategy =
getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
// TODO add justificationMode prop into local Data
int justificationMode = Layout.JUSTIFICATION_MODE_NONE;
TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
return new ReactTextUpdate(
spanned,
@@ -106,25 +102,7 @@ public class ReactTextViewManager
false, // TODO add this into local Data
TextAttributeProps.getTextAlignment(props),
textBreakStrategy,
justificationMode);
}
private int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
if (textBreakStrategy != null) {
switch (textBreakStrategy) {
case "simple":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
break;
case "balanced":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
break;
default:
androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
break;
}
}
return androidTextBreakStrategy;
TextAttributeProps.getJustificationMode(props));
}
@Override
@@ -154,7 +132,8 @@ public class ReactTextViewManager
widthMode,
height,
heightMode,
mReactTextViewManagerCallback);
mReactTextViewManagerCallback,
attachmentsPositions);
}
@Override
@@ -37,10 +37,12 @@ public class TextAttributeProps {
private static final String PROP_TEXT_TRANSFORM = "textTransform";
private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
private static final int DEFAULT_JUSTIFICATION_MODE =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
private static final int DEFAULT_BREAK_STRATEGY =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
protected boolean mIsAttachment = false;
protected float mAttachmentWidth = Float.NaN;
protected float mAttachmentHeight = Float.NaN;
protected float mLineHeight = Float.NaN;
protected boolean mIsColorSet = false;
protected boolean mAllowFontScaling = true;
@@ -54,10 +56,7 @@ public class TextAttributeProps {
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 int mJustificationMode =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
protected TextTransform mTextTransform = TextTransform.UNSET;
protected float mTextShadowOffsetDx = 0;
@@ -126,14 +125,10 @@ public class TextAttributeProps {
setFontVariant(getArrayProp(ViewProps.FONT_VARIANT));
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));
setAttachmentHeight(getFloatProp(ViewProps.HEIGHT, UNSET));
setAttachmentWidth(getFloatProp(ViewProps.WIDTH, UNSET));
setIsAttachment(getBooleanProp(ViewProps.IS_ATTACHMENT, false));
}
// TODO T63645393 add support for RTL
@@ -161,16 +156,15 @@ public class TextAttributeProps {
return textAlignment;
}
private void setIsAttachment(boolean isAttachment) {
mIsAttachment = isAttachment;
}
public static int getJustificationMode(ReactStylesDiffMap props) {
@Nullable
String textAlignPropValue =
props.hasKey(ViewProps.TEXT_ALIGN) ? props.getString(ViewProps.TEXT_ALIGN) : null;
private void setAttachmentWidth(float attachmentWidth) {
mAttachmentWidth = attachmentWidth;
}
private void setAttachmentHeight(float attachmentHeight) {
mAttachmentHeight = attachmentHeight;
if ("justify".equals(textAlignPropValue) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return Layout.JUSTIFICATION_MODE_INTER_WORD;
}
return DEFAULT_JUSTIFICATION_MODE;
}
private boolean getBooleanProp(String name, boolean defaultValue) {
@@ -357,23 +351,6 @@ public class TextAttributeProps {
}
}
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;
@@ -418,6 +395,24 @@ public class TextAttributeProps {
}
}
public static int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY;
if (textBreakStrategy != null) {
switch (textBreakStrategy) {
case "simple":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
break;
case "balanced":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
break;
default:
androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
break;
}
}
return androidTextBreakStrategy;
}
/**
* Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
* return the weight.
@@ -24,6 +24,7 @@ import com.facebook.react.bridge.ReadableArray;
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.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
@@ -41,6 +42,8 @@ public class TextLayoutManager {
// Specifies the amount of spannable that are stored into the {@link sSpannableCache}.
private static final int spannableCacheSize = 100;
private static final String INLINE_VIEW_PLACEHOLDER = "0";
private static final Object sSpannableCacheLock = new Object();
private static LruCache<String, Spannable> sSpannableCache = new LruCache<>(spannableCacheSize);
@@ -60,10 +63,18 @@ public class TextLayoutManager {
sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform));
// TODO: add support for TextInlineImage and BaseText
int end = sb.length();
if (end >= start) {
int reactTag = fragment.getInt("reactTag");
if (fragment.hasKey(ViewProps.IS_ATTACHMENT)
&& fragment.getBoolean(ViewProps.IS_ATTACHMENT)) {
float width = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.WIDTH));
float height = PixelUtil.toPixelFromSP(fragment.getDouble(ViewProps.HEIGHT));
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
} else if (end >= start) {
if (textAttributes.mIsColorSet) {
ops.add(
new SetSpanOperation(
@@ -120,7 +131,6 @@ public class TextLayoutManager {
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
}
int reactTag = fragment.getInt("reactTag");
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
}
}
@@ -189,15 +199,16 @@ public class TextLayoutManager {
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode,
ReactTextViewManagerCallback reactTextViewManagerCallback) {
ReactTextViewManagerCallback reactTextViewManagerCallback,
@Nullable int[] attachmentsPositions) {
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
TextPaint textPaint = sTextPaintInstance;
Spannable preparedSpannableText =
getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback);
// TODO add these props to paragraph attributes
int textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
int textBreakStrategy =
TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
boolean includeFontPadding = true;
if (preparedSpannableText == null) {
@@ -211,6 +222,7 @@ public class TextLayoutManager {
boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0;
Layout layout;
int spanLength = text.length();
if (boring == null
&& (unconstrainedWidth
|| (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
@@ -230,7 +242,7 @@ public class TextLayoutManager {
includeFontPadding);
} else {
layout =
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
@@ -267,7 +279,7 @@ public class TextLayoutManager {
includeFontPadding);
} else {
layout =
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
@@ -282,16 +294,95 @@ public class TextLayoutManager {
? paragraphAttributes.getInt("maximumNumberOfLines")
: UNSET;
width = layout.getWidth();
int calculatedWidth = layout.getWidth();
int calculatedHeight;
if (maximumNumberOfLines != UNSET
&& maximumNumberOfLines != 0
&& maximumNumberOfLines < layout.getLineCount()) {
height = layout.getLineBottom(maximumNumberOfLines - 1);
calculatedHeight = layout.getLineBottom(maximumNumberOfLines - 1);
} else {
height = layout.getHeight();
calculatedHeight = layout.getHeight();
}
return YogaMeasureOutput.make(PixelUtil.toSPFromPixel(width), PixelUtil.toSPFromPixel(height));
// Calculate the positions of the attachments (views) that will be rendered inside the Spanned
// Text. The following logic is only executed when a text contains views inside. This
// follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method).
int attachmentIndex = 0;
int lastAttachmentFoundInSpan;
for (int i = 0; i < spanLength; i = lastAttachmentFoundInSpan) {
lastAttachmentFoundInSpan =
text.nextSpanTransition(i, spanLength, TextInlineViewPlaceholderSpan.class);
TextInlineViewPlaceholderSpan[] placeholders =
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
int start = text.getSpanStart(placeholder);
int line = layout.getLineForOffset(start);
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
// This truncation check works well on recent versions of Android (tested on 5.1.1 and
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the first
// thing to be truncated.
if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line))
|| start >= layout.getLineEnd(line)) {
int placeholderWidth = placeholder.getWidth();
int placeholderHeight = placeholder.getHeight();
// Calculate if the direction of the placeholder character is Right-To-Left.
boolean isRtlChar = layout.isRtlCharAt(start);
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
int placeholderLeftPosition;
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
// the last offset in the layout will result in an endless loop. Work around
// this bug by avoiding getPrimaryHorizontal in that case.
if (start == spanLength - 1) {
placeholderLeftPosition =
isRtlParagraph
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns incorrect
// values when the paragraph is RTL and `setSingleLine(true)`.
? calculatedWidth - (int) layout.getLineWidth(line)
: (int) layout.getLineRight(line) - placeholderWidth;
} else {
// The direction of the paragraph may not be exactly the direction the string is heading
// in at the
// position of the placeholder. So, if the direction of the character is the same as the
// paragraph
// use primary, secondary otherwise.
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
placeholderLeftPosition =
characterAndParagraphDirectionMatch
? (int) layout.getPrimaryHorizontal(start)
: (int) layout.getSecondaryHorizontal(start);
if (isRtlParagraph) {
// Adjust `placeholderLeftPosition` to work around an Android bug.
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
// `getLineRight` return incorrect values. Their return values seem to be off
// by the same number of pixels so subtracting these values cancels out the error.
//
// The result is equivalent to bugless versions of
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
placeholderLeftPosition =
calculatedWidth - ((int) layout.getLineRight(line) - placeholderLeftPosition);
}
if (isRtlChar) {
placeholderLeftPosition -= placeholderWidth;
}
}
// Vertically align the inline view to the baseline of the line of text.
int placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
int attachmentPosition = attachmentIndex * 2;
// The attachment array returns the positions of each of the attachments as
attachmentsPositions[attachmentPosition] =
(int) PixelUtil.toSPFromPixel(placeholderTopPosition);
attachmentsPositions[attachmentPosition + 1] =
(int) PixelUtil.toSPFromPixel(placeholderLeftPosition);
attachmentIndex++;
}
}
}
return YogaMeasureOutput.make(
PixelUtil.toSPFromPixel(calculatedWidth), PixelUtil.toSPFromPixel(calculatedHeight));
}
// TODO T31905686: This class should be private
@@ -1206,26 +1206,6 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
view.setPadding(left, top, right, bottom);
}
// This is copied from ReactTextViewManager
// TODO: this should be from a common class or a static method somewhere
private int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
if (textBreakStrategy != null) {
switch (textBreakStrategy) {
case "simple":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
break;
case "balanced":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
break;
default:
androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
break;
}
}
return androidTextBreakStrategy;
}
/**
* May be overriden by subclasses that would like to provide their own instance of the internal
* {@code EditText} this class uses to determine the expected size of the view.
@@ -1281,10 +1261,7 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
view.getContext(), attributedString, mReactTextViewManagerCallback);
int textBreakStrategy =
getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
// TODO add justificationMode prop into local Data
int justificationMode = Layout.JUSTIFICATION_MODE_NONE;
TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
view.mStateWrapper = stateWrapper;
@@ -1294,7 +1271,7 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
false, // TODO add this into local Data
TextAttributeProps.getTextAlignment(props),
textBreakStrategy,
justificationMode,
TextAttributeProps.getJustificationMode(props),
attributedString);
}
}