Android: Expose textBreakStrategy on Text and TextInput

Summary:
Android has a text API called breakStrategy for controlling how paragraphs are broken up into lines. For example, some modes support automatically hyphenating words so a word can be split across lines while others do not.

One source of complexity is that Android provides different defaults for `breakStrategy` for `TextView` vs `EditText`. `TextView`'s default is `BREAK_STRATEGY_HIGH_QUALITY` while `EditText`'s default is `BREAK_STRATEGY_SIMPLE`.

In addition to exposing `textBreakStrategy`, this change also fixes a couple of rendering glitches with `Text` and `TextInput`. `TextView` and `EditText` have different default values for `breakStrategy` and `hyphenationFrequency` than `StaticLayout`. Consequently, we were using different parameters for measuring and rendering. Whenever measuring and rendering parameters are inconsistent, it can result in visual glitches such as the text taking up too much space or being clipped.

This change fixes these inconsistencies by setting `breakStrategy` and `hyphenat
Closes https://github.com/facebook/react-native/pull/11007

Differential Revision: D4227495

Pulled By: lacker

fbshipit-source-id: c2d96bd0ddc7bd315fda016fb4f1b5108a2e35cf
This commit is contained in:
Adam Comella 2016-12-16 01:19:16 -08:00 committed by Facebook Github Bot
parent c93643c079
commit c0ea23cfb0
8 changed files with 151 additions and 13 deletions

View File

@ -330,6 +330,12 @@ const TextInput = React.createClass({
* The default value is `false`.
*/
multiline: PropTypes.bool,
/**
* Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
* The default value is `simple`.
* @platform android
*/
textBreakStrategy: React.PropTypes.oneOf(['simple', 'highQuality', 'balanced']),
/**
* Callback that is called when the text input is blurred.
*/
@ -724,6 +730,7 @@ const TextInput = React.createClass({
text={this._getText()}
children={children}
disableFullscreenUI={this.props.disableFullscreenUI}
textBreakStrategy={this.props.textBreakStrategy}
/>;
return (

View File

@ -33,6 +33,7 @@ const viewConfig = {
selectable: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
}),
uiViewClassName: 'RCTText',
};
@ -116,6 +117,12 @@ const Text = React.createClass({
* This prop is commonly used with `ellipsizeMode`.
*/
numberOfLines: React.PropTypes.number,
/**
* Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
* The default value is `highQuality`.
* @platform android
*/
textBreakStrategy: React.PropTypes.oneOf(['simple', 'highQuality', 'balanced']),
/**
* Invoked on mount and layout changes with
*

View File

@ -82,6 +82,7 @@ public class ViewProps {
public static final String TEXT_ALIGN = "textAlign";
public static final String TEXT_ALIGN_VERTICAL = "textAlignVertical";
public static final String TEXT_DECORATION_LINE = "textDecorationLine";
public static final String TEXT_BREAK_STRATEGY = "textBreakStrategy";
public static final String ALLOW_FONT_SCALING = "allowFontScaling";

View File

@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.List;
import android.graphics.Typeface;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
@ -248,14 +249,27 @@ public class ReactTextShadowNode extends LayoutShadowNode {
(!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,
(int) Math.ceil(desiredWidth),
hintWidth,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
true);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(true)
.setBreakStrategy(mTextBreakStrategy)
.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.
@ -270,6 +284,8 @@ public class ReactTextShadowNode extends LayoutShadowNode {
true);
} 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,
@ -278,6 +294,15 @@ public class ReactTextShadowNode extends LayoutShadowNode {
1.f,
0.f,
true);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(true)
.setBreakStrategy(mTextBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.build();
}
}
if (mNumberOfLines != UNSET &&
@ -317,6 +342,8 @@ public class ReactTextShadowNode extends LayoutShadowNode {
protected float mFontSizeInput = UNSET;
protected int mLineHeightInput = UNSET;
protected int mTextAlign = Gravity.NO_GRAVITY;
protected int mTextBreakStrategy = (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ?
0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
private float mTextShadowOffsetDx = 0;
private float mTextShadowOffsetDy = 0;
@ -549,6 +576,25 @@ public class ReactTextShadowNode extends LayoutShadowNode {
markUpdated();
}
@ReactProp(name = ViewProps.TEXT_BREAK_STRATEGY)
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);
}
markUpdated();
}
@ReactProp(name = PROP_SHADOW_OFFSET)
public void setTextShadowOffset(ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
@ -607,7 +653,8 @@ public class ReactTextShadowNode extends LayoutShadowNode {
getPadding(Spacing.TOP),
getPadding(Spacing.END),
getPadding(Spacing.BOTTOM),
getTextAlign()
getTextAlign(),
mTextBreakStrategy
);
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}

View File

@ -9,6 +9,7 @@
package com.facebook.react.views.text;
import android.text.Layout;
import android.text.Spannable;
/**
@ -26,7 +27,13 @@ public class ReactTextUpdate {
private final float mPaddingRight;
private final float mPaddingBottom;
private final int mTextAlign;
private final int mTextBreakStrategy;
/**
* @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains
* because it's being used by a unit test that isn't currently open source.
*/
@Deprecated
public ReactTextUpdate(
Spannable text,
int jsEventCounter,
@ -36,6 +43,27 @@ public class ReactTextUpdate {
float paddingEnd,
float paddingBottom,
int textAlign) {
this(text,
jsEventCounter,
containsImages,
paddingStart,
paddingTop,
paddingEnd,
paddingBottom,
textAlign,
Layout.BREAK_STRATEGY_HIGH_QUALITY);
}
public ReactTextUpdate(
Spannable text,
int jsEventCounter,
boolean containsImages,
float paddingStart,
float paddingTop,
float paddingEnd,
float paddingBottom,
int textAlign,
int textBreakStrategy) {
mText = text;
mJsEventCounter = jsEventCounter;
mContainsImages = containsImages;
@ -44,6 +72,7 @@ public class ReactTextUpdate {
mPaddingRight = paddingEnd;
mPaddingBottom = paddingBottom;
mTextAlign = textAlign;
mTextBreakStrategy = textBreakStrategy;
}
public Spannable getText() {
@ -77,4 +106,8 @@ public class ReactTextUpdate {
public int getTextAlign() {
return mTextAlign;
}
public int getTextBreakStrategy() {
return mTextBreakStrategy;
}
}

View File

@ -15,6 +15,7 @@ import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
@ -69,6 +70,11 @@ public class ReactTextView extends TextView implements ReactCompoundView {
mTextAlign = nextTextAlign;
}
setGravityHorizontal(mTextAlign);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (getBreakStrategy() != update.getTextBreakStrategy()) {
setBreakStrategy(update.getTextBreakStrategy());
}
}
}
@Override

View File

@ -19,6 +19,7 @@ import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
@ -340,6 +341,11 @@ public class ReactEditText extends EditText {
mIsSettingTextFromJS = true;
getText().replace(0, length(), spannableStringBuilder);
mIsSettingTextFromJS = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (getBreakStrategy() != reactTextUpdate.getTextBreakStrategy()) {
setBreakStrategy(reactTextUpdate.getTextBreakStrategy());
}
}
}
/**

View File

@ -10,7 +10,10 @@
package com.facebook.react.views.textinput;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
import android.util.TypedValue;
import android.view.ViewGroup;
@ -22,12 +25,14 @@ import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaNodeAPI;
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.MeasureUtil;
import com.facebook.react.views.text.ReactTextShadowNode;
@ -42,6 +47,8 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
private int mJsEventCount = UNSET;
public ReactTextInputShadowNode() {
mTextBreakStrategy = (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ?
0 : Layout.BREAK_STRATEGY_SIMPLE;
setMeasureFunction(this);
}
@ -100,6 +107,12 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
editText.setLines(mNumberOfLines);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (editText.getBreakStrategy() != mTextBreakStrategy) {
editText.setBreakStrategy(mTextBreakStrategy);
}
}
editText.measure(
MeasureUtil.getMeasureSpec(width, widthMode),
MeasureUtil.getMeasureSpec(height, heightMode));
@ -118,6 +131,23 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
mJsEventCount = mostRecentEventCount;
}
@Override
public void setTextBreakStrategy(@Nullable String textBreakStrategy) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
if (textBreakStrategy == null || "simple".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
} else if ("highQuality".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
} else if ("balanced".equals(textBreakStrategy)) {
mTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
} else {
throw new JSApplicationIllegalArgumentException("Invalid textBreakStrategy: " + textBreakStrategy);
}
}
@Override
public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
super.onCollectExtraUpdates(uiViewOperationQueue);
@ -146,7 +176,8 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
getPadding(Spacing.TOP),
getPadding(Spacing.END),
getPadding(Spacing.BOTTOM),
mTextAlign
mTextAlign,
mTextBreakStrategy
);
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}