Skip to content

Commit

Permalink
Android: Expose textBreakStrategy on Text and TextInput
Browse files Browse the repository at this point in the history
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 facebook#11007

Differential Revision: D4227495

Pulled By: lacker

fbshipit-source-id: c2d96bd0ddc7bd315fda016fb4f1b5108a2e35cf
  • Loading branch information
Adam Comella authored and facebook-github-bot committed Dec 16, 2016
1 parent c93643c commit c0ea23c
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 13 deletions.
7 changes: 7 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -724,6 +730,7 @@ const TextInput = React.createClass({
text={this._getText()}
children={children}
disableFullscreenUI={this.props.disableFullscreenUI}
textBreakStrategy={this.props.textBreakStrategy}
/>;

return (
Expand Down
7 changes: 7 additions & 0 deletions Libraries/Text/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const viewConfig = {
selectable: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
}),
uiViewClassName: 'RCTText',
};
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;

import android.graphics.Typeface;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
Expand Down Expand Up @@ -248,14 +249,27 @@ public long measure(
(!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.
layout = new StaticLayout(

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.
Expand All @@ -270,14 +284,25 @@ public long measure(
true);
} else {
// Is used for multiline, boring text and the width is known.
layout = new StaticLayout(
text,
textPaint,
(int) width,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
true);

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
layout = new StaticLayout(
text,
textPaint,
(int) width,
Layout.Alignment.ALIGN_NORMAL,
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 &&
Expand Down Expand Up @@ -317,6 +342,8 @@ private static int parseNumericFontWeight(String fontWeightString) {
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;
Expand Down Expand Up @@ -549,6 +576,25 @@ public void setTextDecorationLine(@Nullable String textDecorationLineString) {
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;
Expand Down Expand Up @@ -607,7 +653,8 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
getPadding(Spacing.TOP),
getPadding(Spacing.END),
getPadding(Spacing.BOTTOM),
getTextAlign()
getTextAlign(),
mTextBreakStrategy
);
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package com.facebook.react.views.text;

import android.text.Layout;
import android.text.Spannable;

/**
Expand All @@ -26,6 +27,32 @@ 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,
boolean containsImages,
float paddingStart,
float paddingTop,
float paddingEnd,
float paddingBottom,
int textAlign) {
this(text,
jsEventCounter,
containsImages,
paddingStart,
paddingTop,
paddingEnd,
paddingBottom,
textAlign,
Layout.BREAK_STRATEGY_HIGH_QUALITY);
}

public ReactTextUpdate(
Spannable text,
Expand All @@ -35,7 +62,8 @@ public ReactTextUpdate(
float paddingTop,
float paddingEnd,
float paddingBottom,
int textAlign) {
int textAlign,
int textBreakStrategy) {
mText = text;
mJsEventCounter = jsEventCounter;
mContainsImages = containsImages;
Expand All @@ -44,6 +72,7 @@ public ReactTextUpdate(
mPaddingRight = paddingEnd;
mPaddingBottom = paddingBottom;
mTextAlign = textAlign;
mTextBreakStrategy = textBreakStrategy;
}

public Spannable getText() {
Expand Down Expand Up @@ -77,4 +106,8 @@ public float getPaddingBottom() {
public int getTextAlign() {
return mTextAlign;
}

public int getTextBreakStrategy() {
return mTextBreakStrategy;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand Down Expand Up @@ -69,6 +70,11 @@ public void setText(ReactTextUpdate update) {
mTextAlign = nextTextAlign;
}
setGravityHorizontal(mTextAlign);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (getBreakStrategy() != update.getTextBreakStrategy()) {
setBreakStrategy(update.getTextBreakStrategy());
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand Down Expand Up @@ -340,6 +341,11 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
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());
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,12 +25,14 @@
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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -100,6 +107,12 @@ public long measure(
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));
Expand All @@ -118,6 +131,23 @@ public void setMostRecentEventCount(int mostRecentEventCount) {
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);
Expand Down Expand Up @@ -146,7 +176,8 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
getPadding(Spacing.TOP),
getPadding(Spacing.END),
getPadding(Spacing.BOTTOM),
mTextAlign
mTextAlign,
mTextBreakStrategy
);
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}
Expand Down

0 comments on commit c0ea23c

Please sign in to comment.