diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index be27e77d13956a..918716670230f5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -270,6 +270,10 @@ private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode w .setBreakStrategy(mTextBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setJustificationMode(mJustificationMode); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { builder.setUseLineSpacingFromFallbacks(true); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index fbbc11768a3daa..cb735fc85630a5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -380,8 +380,9 @@ protected void onDraw(Canvas canvas) { getBreakStrategy(), getHyphenationFrequency(), // always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on - // how exacly lines are aligned, just their width + // how exactly lines are aligned, just their width Layout.Alignment.ALIGN_NORMAL, + (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? -1 : getJustificationMode(), getPaint()); setText(getSpanned()); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 63bb805fcac1c4..882114336146aa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -146,13 +146,41 @@ public static boolean isRTL(MapBuffer attributedString) { == LayoutDirection.RTL; } - private static Layout.Alignment getTextAlignment(MapBuffer attributedString, Spannable spanned) { + @Nullable + private static String getTextAlignmentAttr(MapBuffer attributedString) { // TODO: Don't read AS_KEY_FRAGMENTS, which may be expensive, and is not present when using // cached Spannable if (!attributedString.contains(AS_KEY_FRAGMENTS)) { - return Layout.Alignment.ALIGN_NORMAL; + return null; } + MapBuffer fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS); + if (fragments.getCount() != 0) { + MapBuffer fragment = fragments.getMapBuffer(0); + MapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES); + + if (textAttributes.contains(TextAttributeProps.TA_KEY_ALIGNMENT)) { + return textAttributes.getString(TextAttributeProps.TA_KEY_ALIGNMENT); + } + } + + return null; + } + + private static int getTextJustificationMode(@Nullable String alignmentAttr) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return -1; + } + + if (alignmentAttr != null && alignmentAttr.equals("justified")) { + return Layout.JUSTIFICATION_MODE_INTER_WORD; + } + + return Layout.JUSTIFICATION_MODE_NONE; + } + + private static Layout.Alignment getTextAlignment( + MapBuffer attributedString, Spannable spanned, @Nullable String alignmentAttr) { // Android will align text based on the script, so normal and opposite alignment needs to be // swapped when the directions of paragraph and script don't match. // I.e. paragraph is LTR but script is RTL, text needs to be aligned to the left, which means @@ -165,23 +193,15 @@ private static Layout.Alignment getTextAlignment(MapBuffer attributedString, Spa Layout.Alignment alignment = swapNormalAndOpposite ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL; - MapBuffer fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS); - if (fragments.getCount() != 0) { - MapBuffer fragment = fragments.getMapBuffer(0); - MapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES); + if (alignmentAttr == null) { + return alignment; + } - if (textAttributes.contains(TextAttributeProps.TA_KEY_ALIGNMENT)) { - String alignmentAttr = textAttributes.getString(TextAttributeProps.TA_KEY_ALIGNMENT); - - if (alignmentAttr.equals("center")) { - alignment = Layout.Alignment.ALIGN_CENTER; - } else if (alignmentAttr.equals("right")) { - alignment = - swapNormalAndOpposite - ? Layout.Alignment.ALIGN_NORMAL - : Layout.Alignment.ALIGN_OPPOSITE; - } - } + if (alignmentAttr.equals("center")) { + alignment = Layout.Alignment.ALIGN_CENTER; + } else if (alignmentAttr.equals("right")) { + alignment = + swapNormalAndOpposite ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE; } return alignment; @@ -190,7 +210,8 @@ private static Layout.Alignment getTextAlignment(MapBuffer attributedString, Spa public static int getTextGravity( MapBuffer attributedString, Spannable spanned, int defaultValue) { int gravity = defaultValue; - Layout.Alignment alignment = getTextAlignment(attributedString, spanned); + @Nullable String alignmentAttr = getTextAlignmentAttr(attributedString); + Layout.Alignment alignment = getTextAlignment(attributedString, spanned, alignmentAttr); // depending on whether the script is LTR or RTL, ALIGN_NORMAL and ALIGN_OPPOSITE may mean // different things @@ -363,6 +384,7 @@ private static Layout createLayout( int textBreakStrategy, int hyphenationFrequency, Layout.Alignment alignment, + int justificationMode, TextPaint paint) { Layout layout; @@ -420,6 +442,10 @@ private static Layout createLayout( .setTextDirection( isScriptRTL ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setJustificationMode(justificationMode); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { builder.setUseLineSpacingFromFallbacks(true); } @@ -506,7 +532,9 @@ private static Layout createLayout( ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) : ReactConstants.UNSET; - Layout.Alignment alignment = getTextAlignment(attributedString, text); + @Nullable String alignmentAttr = getTextAlignmentAttr(attributedString); + Layout.Alignment alignment = getTextAlignment(attributedString, text, alignmentAttr); + int justificationMode = getTextJustificationMode(alignmentAttr); if (adjustFontSizeToFit) { double minimumFontSize = @@ -526,6 +554,7 @@ private static Layout createLayout( textBreakStrategy, hyphenationFrequency, alignment, + justificationMode, paint); } @@ -538,6 +567,7 @@ private static Layout createLayout( textBreakStrategy, hyphenationFrequency, alignment, + justificationMode, paint); } @@ -553,6 +583,7 @@ private static Layout createLayout( int textBreakStrategy, int hyphenationFrequency, Layout.Alignment alignment, + int justificationMode, TextPaint paint) { BoringLayout.Metrics boring = BoringLayout.isBoring(text, paint); Layout layout = @@ -565,6 +596,7 @@ private static Layout createLayout( textBreakStrategy, hyphenationFrequency, alignment, + justificationMode, paint); // Minimum font size is 4pts to match the iOS implementation. @@ -613,6 +645,7 @@ private static Layout createLayout( textBreakStrategy, hyphenationFrequency, alignment, + justificationMode, paint); } } diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index 751052f4ec8121..b9cb1a2f126ced 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -696,7 +696,7 @@ function NestedExample(props: {}): React.Node { function TextAlignExample(props: {}): React.Node { return ( - <> + auto (default) - english LTR أحب اللغة العربية auto (default) - arabic RTL @@ -717,7 +717,7 @@ function TextAlignExample(props: {}): React.Node { as you can see all of the lines except the last one span the available width of the parent container. - + ); } diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 0316e0b47eb36d..6b65d167894f26 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -60,7 +60,9 @@ class TextAlignRTLExample extends React.Component< const {isRTL} = this.state; const toggleRTL = () => this.setState({isRTL: !isRTL}); return ( - + auto (default) - english LTR {'\u0623\u062D\u0628 \u0627\u0644\u0644\u063A\u0629 ' + @@ -1254,6 +1256,7 @@ const examples = [ }, { title: 'Text Align with RTL', + name: 'textAlign', render: function (): React.Node { return ; },