diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 16e2ae8cb..471315496 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -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 ( diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 8c990d0d5..114732763 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -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 * diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index f3faee509..5c4feaf02 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -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"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 1a38a51f3..8df4adcbd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -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. - 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. @@ -270,14 +284,25 @@ public class ReactTextShadowNode extends LayoutShadowNode { 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 && @@ -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); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 237be1fe8..9f67aec61 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -9,6 +9,7 @@ package com.facebook.react.views.text; +import android.text.Layout; import android.text.Spannable; /** @@ -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, @@ -35,7 +62,8 @@ public class ReactTextUpdate { float paddingTop, float paddingEnd, float paddingBottom, - int textAlign) { + 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; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 50828a80b..3d5d9668b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 90af968f0..9e9fc6edc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -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()); + } + } } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index 3536f3770..409cf13e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -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); }