diff --git a/RNTester/js/TextExample.android.js b/RNTester/js/TextExample.android.js index 6726943b261f52..22b291b7770e72 100644 --- a/RNTester/js/TextExample.android.js +++ b/RNTester/js/TextExample.android.js @@ -325,6 +325,12 @@ class TextExample extends React.Component<{}> { right right right right right right right right right right right right right + + justify (works when api level >= 26 otherwise fallbacks to "left"): + this text component{"'"}s contents are laid out with "textAlign: + justify" and as you can see all of the lines except the last one + span the available width of the parent container. + diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index ec00cd5b30662e..5b9d5b21df52fd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -265,6 +265,9 @@ private static int parseNumericFontWeight(String fontWeightString) { 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; protected float mTextShadowOffsetDy = 0; @@ -357,19 +360,28 @@ public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) { @ReactProp(name = ViewProps.TEXT_ALIGN) 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 + if ("justify".equals(textAlign)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mJustificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD; + } mTextAlign = Gravity.LEFT; } else { - throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; + } + + 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 { + throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + } + } markUpdated(); } 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 eaef5690304b33..5cf16db80f0fd4 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 @@ -99,14 +99,18 @@ public long measure( new StaticLayout( text, textPaint, hintWidth, alignment, 1.f, 0.f, mIncludeFontPadding); } else { - layout = + StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth) - .setAlignment(alignment) - .setLineSpacing(0.f, 1.f) - .setIncludePad(mIncludeFontPadding) - .setBreakStrategy(mTextBreakStrategy) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(); + .setAlignment(alignment) + .setLineSpacing(0.f, 1.f) + .setIncludePad(mIncludeFontPadding) + .setBreakStrategy(mTextBreakStrategy) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setJustificationMode(mJustificationMode); + } + layout = builder.build(); } } else if (boring != null && (unconstrainedWidth || boring.width <= width)) { @@ -217,7 +221,8 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { getPadding(Spacing.END), getPadding(Spacing.BOTTOM), getTextAlign(), - mTextBreakStrategy); + mTextBreakStrategy, + mJustificationMode); 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 c31bb3c55ea52d..fd1344f0fb714d 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 @@ -26,6 +26,7 @@ public class ReactTextUpdate { private final float mPaddingBottom; private final int mTextAlign; private final int mTextBreakStrategy; + private final int mJustificationMode; /** * @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains @@ -49,7 +50,8 @@ public ReactTextUpdate( paddingEnd, paddingBottom, textAlign, - Layout.BREAK_STRATEGY_HIGH_QUALITY); + Layout.BREAK_STRATEGY_HIGH_QUALITY, + Layout.JUSTIFICATION_MODE_NONE); } public ReactTextUpdate( @@ -61,7 +63,8 @@ public ReactTextUpdate( float paddingEnd, float paddingBottom, int textAlign, - int textBreakStrategy) { + int textBreakStrategy, + int justificationMode) { mText = text; mJsEventCounter = jsEventCounter; mContainsImages = containsImages; @@ -71,6 +74,7 @@ public ReactTextUpdate( mPaddingBottom = paddingBottom; mTextAlign = textAlign; mTextBreakStrategy = textBreakStrategy; + mJustificationMode = justificationMode; } public Spannable getText() { @@ -108,4 +112,8 @@ public int getTextAlign() { public int getTextBreakStrategy() { return mTextBreakStrategy; } + + public int getJustificationMode() { + return mJustificationMode; + } } 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 7e99cb1429d8df..a804a46a94a6d5 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 @@ -72,6 +72,11 @@ public void setText(ReactTextUpdate update) { setBreakStrategy(update.getTextBreakStrategy()); } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (getJustificationMode() != update.getJustificationMode()) { + setJustificationMode(update.getJustificationMode()); + } + } } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index eb0e9f9c0cee03..9241d4f4775e4d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -79,6 +79,9 @@ public Object updateLocalData( // TODO add textBreakStrategy prop into local Data int textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; + // TODO add justificationMode prop into local Data + int justificationMode = Layout.JUSTIFICATION_MODE_NONE; + return new ReactTextUpdate( spanned, -1, // TODO add this into local Data? @@ -88,7 +91,9 @@ public Object updateLocalData( textViewProps.getEndPadding(), textViewProps.getBottomPadding(), textViewProps.getTextAlign(), - textBreakStrategy); + textBreakStrategy, + justificationMode + ); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index cbf49e34dc0e84..e23961d75d8b44 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -50,6 +50,8 @@ public class TextAttributeProps { 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; @@ -204,19 +206,28 @@ public void setAllowFontScaling(boolean allowFontScaling) { } 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 + if ("justify".equals(textAlign)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mJustificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD; + } mTextAlign = Gravity.LEFT; } else { - throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; + } + + 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 { + throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 9cb5990ed152a1..4c9999bc661bd3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -15,6 +15,7 @@ import android.text.Editable; import android.text.InputFilter; import android.text.InputType; +import android.text.Layout; import android.text.Spannable; import android.text.TextWatcher; import android.util.TypedValue; @@ -455,19 +456,28 @@ public void setUnderlineColor(ReactEditText view, @Nullable Integer underlineCol @ReactProp(name = ViewProps.TEXT_ALIGN) public void setTextAlign(ReactEditText view, @Nullable String textAlign) { - if (textAlign == null || "auto".equals(textAlign)) { - view.setGravityHorizontal(Gravity.NO_GRAVITY); - } else if ("left".equals(textAlign)) { - view.setGravityHorizontal(Gravity.LEFT); - } else if ("right".equals(textAlign)) { - view.setGravityHorizontal(Gravity.RIGHT); - } else if ("center".equals(textAlign)) { - view.setGravityHorizontal(Gravity.CENTER_HORIZONTAL); - } else if ("justify".equals(textAlign)) { - // Fallback gracefully for cross-platform compat instead of error + if ("justify".equals(textAlign)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + view.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD); + } view.setGravityHorizontal(Gravity.LEFT); } else { - throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + view.setJustificationMode(Layout.JUSTIFICATION_MODE_NONE); + } + + if (textAlign == null || "auto".equals(textAlign)) { + view.setGravityHorizontal(Gravity.NO_GRAVITY); + } else if ("left".equals(textAlign)) { + view.setGravityHorizontal(Gravity.LEFT); + } else if ("right".equals(textAlign)) { + view.setGravityHorizontal(Gravity.RIGHT); + } else if ("center".equals(textAlign)) { + view.setGravityHorizontal(Gravity.CENTER_HORIZONTAL); + } else { + throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + } + } } 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 53efe02e06e46d..f40a05dde07cd9 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 @@ -204,7 +204,8 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { getPadding(Spacing.RIGHT), getPadding(Spacing.BOTTOM), mTextAlign, - mTextBreakStrategy); + mTextBreakStrategy, + mJustificationMode); uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); } } diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java index 7938bbffd2625f..05af680b549ab0 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java @@ -15,6 +15,7 @@ import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; @@ -419,6 +420,21 @@ public void testMaxLinesApplied() { assertThat(textView.getEllipsize()).isEqualTo(TextUtils.TruncateAt.END); } + @TargetApi(Build.VERSION_CODES.O) + @Test + public void testTextAlignJustifyApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + JavaOnlyMap.of("textAlign", "justify"), + JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, "test text")); + + TextView textView = (TextView) rootView.getChildAt(0); + assertThat(textView.getText().toString()).isEqualTo("test text"); + assertThat(textView.getJustificationMode()).isEqualTo(Layout.JUSTIFICATION_MODE_INTER_WORD); + } + /** * Make sure TextView has exactly one span and that span has given type. */ diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java index 50fc92e218d3ed..3bf3865b6e298d 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java @@ -9,8 +9,10 @@ import android.content.res.ColorStateList; import android.graphics.Color; +import android.os.Build; import android.text.InputType; import android.text.InputFilter; +import android.text.Layout; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.inputmethod.EditorInfo; @@ -344,6 +346,10 @@ public void testTextAlign() { assertThat(view.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK).isEqualTo(Gravity.CENTER_HORIZONTAL); mManager.updateProperties(view, buildStyles("textAlign", null)); assertThat(view.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK).isEqualTo(defaultHorizontalGravity); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mManager.updateProperties(view, buildStyles("textAlign", "justify")); + assertThat(view.getJustificationMode()).isEqualTo(Layout.JUSTIFICATION_MODE_INTER_WORD); + } // TextAlignVertical mManager.updateProperties(view, buildStyles("textAlignVertical", "top"));