Skip to content

Commit f938637

Browse files
iray-tnoclaude
andcommitted
Add android_lineBreakStyle and android_lineBreakWordStyle to Text
Android 13 (API 33) added LineBreakConfig, which selects the strictness of the line-breaking rules (CSS `line-break`: loose, normal, strict) and phrase-based breaking for Japanese (CSS `word-break: auto-phrase`). React Native measures and draws Text with a StaticLayout it builds itself, so neither setting can be reached from outside: a native module could only change the drawn TextView, which would then disagree with the height Yoga measured. Add two Android-only paragraph props, plumbed the same way as android_hyphenationFrequency: - android_lineBreakStyle: 'none' | 'loose' | 'normal' | 'strict' - android_lineBreakWordStyle: 'none' | 'phrase' They go into ParagraphAttributes and the paragraph MapBuffer, are applied to the StaticLayout.Builder in TextLayoutManager (measurement and the prepared layout), and to ReactTextView through @ReactProp setters (drawing). Both are ignored below API 33. When both are 'none' (the default) no LineBreakConfig is set, so existing text lays out exactly as before. TextInput is left out: it does not list the props in its view config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cb5ea03 commit f938637

20 files changed

Lines changed: 434 additions & 4 deletions

File tree

‎packages/react-native/Libraries/Text/TextNativeComponent.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const textViewConfig = {
4747
onTextLayout: true,
4848
dataDetectorType: true,
4949
android_hyphenationFrequency: true,
50+
android_lineBreakStyle: true,
51+
android_lineBreakWordStyle: true,
5052
lineBreakStrategyIOS: true,
5153
},
5254
directEventTypes: {

‎packages/react-native/Libraries/Text/TextProps.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,26 @@ type TextBaseProps = Readonly<{
149149
*/
150150
android_hyphenationFrequency?: ?('normal' | 'none' | 'full'),
151151

152+
/**
153+
* Sets the strictness of the line-breaking rules, such as which characters
154+
* may start or end a line in Chinese and Japanese text (kinsoku). Matches CSS
155+
* `line-break`. Ignored below Android 13 (API level 33).
156+
*
157+
* @default `'none'`
158+
* @platform android
159+
*/
160+
android_lineBreakStyle?: ?('none' | 'loose' | 'normal' | 'strict'),
161+
162+
/**
163+
* Sets word-based line breaking. `'phrase'` breaks Japanese text at phrase
164+
* boundaries instead of between any two characters. Matches CSS
165+
* `word-break: auto-phrase`. Ignored below Android 13 (API level 33).
166+
*
167+
* @default `'none'`
168+
* @platform android
169+
*/
170+
android_lineBreakWordStyle?: ?('none' | 'phrase'),
171+
152172
children?: ?React.Node,
153173

154174
/**

‎packages/react-native/ReactAndroid/api/ReactAndroid.api‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6042,6 +6042,8 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
60426042
public fun setHyphenationFrequency (I)V
60436043
public fun setIncludeFontPadding (Z)V
60446044
public fun setLetterSpacing (F)V
6045+
public fun setLineBreakStyle (I)V
6046+
public fun setLineBreakWordStyle (I)V
60456047
public fun setLinkifyMask (I)V
60466048
public fun setMinimumFontScale (F)V
60476049
public fun setMinimumFontSize (F)V
@@ -6077,6 +6079,8 @@ public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/r
60776079
public final fun setAccessible (Lcom/facebook/react/views/text/ReactTextView;Z)V
60786080
public final fun setAdjustFontSizeToFit (Lcom/facebook/react/views/text/ReactTextView;Z)V
60796081
public final fun setAndroidHyphenationFrequency (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
6082+
public final fun setAndroidLineBreakStyle (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
6083+
public final fun setAndroidLineBreakWordStyle (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
60806084
public final fun setBorderColor (Lcom/facebook/react/views/text/ReactTextView;ILjava/lang/Integer;)V
60816085
public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;IF)V
60826086
public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;ILcom/facebook/react/bridge/Dynamic;)V

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
package com.facebook.react.views.text;
99

10+
import android.annotation.SuppressLint;
1011
import android.content.Context;
1112
import android.graphics.Canvas;
1213
import android.graphics.Rect;
14+
import android.graphics.text.LineBreakConfig;
1315
import android.os.Build;
1416
import android.text.Layout;
1517
import android.text.Spannable;
@@ -26,6 +28,7 @@
2628
import android.view.ViewGroup;
2729
import android.widget.TextView;
2830
import androidx.annotation.Nullable;
31+
import androidx.annotation.RequiresApi;
2932
import androidx.appcompat.widget.AppCompatTextView;
3033
import androidx.core.view.AccessibilityDelegateCompat;
3134
import androidx.core.view.ViewCompat;
@@ -197,6 +200,10 @@ private void initView() {
197200
}
198201

199202
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
203+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
204+
setLineBreakStyle(LineBreakConfig.LINE_BREAK_STYLE_NONE);
205+
setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE);
206+
}
200207
updateView(); // call after changing ellipsizeLocation in particular
201208
}
202209

@@ -249,6 +256,8 @@ protected void onDraw(Canvas canvas) {
249256
getIncludeFontPadding(),
250257
getBreakStrategy(),
251258
getHyphenationFrequency(),
259+
getLineBreakStyleCompat(),
260+
getLineBreakWordStyleCompat(),
252261
// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
253262
// how exactly lines are aligned, just their width
254263
Layout.Alignment.ALIGN_NORMAL,
@@ -597,6 +606,34 @@ public void setHyphenationFrequency(int hyphenationFrequency) {
597606
mShouldAdjustSpannableFontSize = true;
598607
}
599608

609+
@Override
610+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
611+
public void setLineBreakStyle(int lineBreakStyle) {
612+
super.setLineBreakStyle(lineBreakStyle);
613+
mShouldAdjustSpannableFontSize = true;
614+
}
615+
616+
@Override
617+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
618+
public void setLineBreakWordStyle(int lineBreakWordStyle) {
619+
super.setLineBreakWordStyle(lineBreakWordStyle);
620+
mShouldAdjustSpannableFontSize = true;
621+
}
622+
623+
@SuppressLint("InlinedApi")
624+
private int getLineBreakStyleCompat() {
625+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
626+
? getLineBreakStyle()
627+
: LineBreakConfig.LINE_BREAK_STYLE_NONE;
628+
}
629+
630+
@SuppressLint("InlinedApi")
631+
private int getLineBreakWordStyleCompat() {
632+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
633+
? getLineBreakWordStyle()
634+
: LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE;
635+
}
636+
600637
@Override
601638
public void setLetterSpacing(float letterSpacing) {
602639
if (Float.isNaN(letterSpacing)) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,20 @@ public constructor(
343343
}
344344
}
345345

346+
@ReactProp(name = "android_lineBreakStyle")
347+
public fun setAndroidLineBreakStyle(view: ReactTextView, lineBreakStyle: String?) {
348+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
349+
view.lineBreakStyle = TextAttributeProps.getLineBreakStyle(lineBreakStyle)
350+
}
351+
}
352+
353+
@ReactProp(name = "android_lineBreakWordStyle")
354+
public fun setAndroidLineBreakWordStyle(view: ReactTextView, lineBreakWordStyle: String?) {
355+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
356+
view.lineBreakWordStyle = TextAttributeProps.getLineBreakWordStyle(lineBreakWordStyle)
357+
}
358+
}
359+
346360
@ReactPropGroup(
347361
names =
348362
[

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
package com.facebook.react.views.text
99

10+
import android.annotation.SuppressLint
11+
import android.graphics.text.LineBreakConfig
1012
import android.os.Build
1113
import android.text.Layout
1214
import android.text.TextUtils.TruncateAt
@@ -612,6 +614,34 @@ public class TextAttributeProps private constructor() {
612614
else -> Layout.HYPHENATION_FREQUENCY_FULL
613615
}
614616

617+
// The LineBreakConfig constants are inlined at compile time, so reading them is safe below API
618+
// 33. Callers apply the result only on API 33+.
619+
@SuppressLint("InlinedApi")
620+
internal fun getLineBreakStyle(lineBreakStyle: String?): Int =
621+
when (lineBreakStyle) {
622+
null,
623+
"none" -> LineBreakConfig.LINE_BREAK_STYLE_NONE
624+
"loose" -> LineBreakConfig.LINE_BREAK_STYLE_LOOSE
625+
"normal" -> LineBreakConfig.LINE_BREAK_STYLE_NORMAL
626+
"strict" -> LineBreakConfig.LINE_BREAK_STYLE_STRICT
627+
else -> {
628+
FLog.w(ReactConstants.TAG, "Invalid android_lineBreakStyle: $lineBreakStyle")
629+
LineBreakConfig.LINE_BREAK_STYLE_NONE
630+
}
631+
}
632+
633+
@SuppressLint("InlinedApi")
634+
internal fun getLineBreakWordStyle(lineBreakWordStyle: String?): Int =
635+
when (lineBreakWordStyle) {
636+
null,
637+
"none" -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
638+
"phrase" -> LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE
639+
else -> {
640+
FLog.w(ReactConstants.TAG, "Invalid android_lineBreakWordStyle: $lineBreakWordStyle")
641+
LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
642+
}
643+
}
644+
615645
public fun getEllipsizeMode(ellipsizeMode: String?): TruncateAt? =
616646
when (ellipsizeMode) {
617647
"head" -> TruncateAt.START

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.content.res.AssetManager
1111
import android.graphics.Color
1212
import android.graphics.RectF
1313
import android.graphics.Typeface
14+
import android.graphics.text.LineBreakConfig
1415
import android.os.Build
1516
import android.text.BoringLayout
1617
import android.text.Layout
@@ -99,6 +100,8 @@ internal object TextLayoutManager {
99100
const val PA_KEY_TEXT_ALIGN_VERTICAL: Int = 8
100101
const val PA_KEY_TEXT_WIDTH_MODE: Int = 9
101102
const val PA_KEY_MINIMUM_FONT_SCALE: Int = 10
103+
const val PA_KEY_LINE_BREAK_STYLE: Int = 11
104+
const val PA_KEY_LINE_BREAK_WORD_STYLE: Int = 12
102105

103106
private val TAG: String = TextLayoutManager::class.java.simpleName
104107

@@ -849,6 +852,8 @@ internal object TextLayoutManager {
849852
includeFontPadding: Boolean,
850853
textBreakStrategy: Int,
851854
hyphenationFrequency: Int,
855+
lineBreakStyle: Int,
856+
lineBreakWordStyle: Int,
852857
alignment: Layout.Alignment,
853858
justificationMode: Int,
854859
ellipsizeMode: TextUtils.TruncateAt?,
@@ -895,6 +900,8 @@ internal object TextLayoutManager {
895900
includeFontPadding,
896901
textBreakStrategy,
897902
hyphenationFrequency,
903+
lineBreakStyle,
904+
lineBreakWordStyle,
898905
alignment,
899906
justificationMode,
900907
ellipsizeMode,
@@ -913,6 +920,8 @@ internal object TextLayoutManager {
913920
includeFontPadding,
914921
textBreakStrategy,
915922
hyphenationFrequency,
923+
lineBreakStyle,
924+
lineBreakWordStyle,
916925
alignment,
917926
justificationMode,
918927
ellipsizeMode,
@@ -963,6 +972,8 @@ internal object TextLayoutManager {
963972
includeFontPadding: Boolean,
964973
textBreakStrategy: Int,
965974
hyphenationFrequency: Int,
975+
lineBreakStyle: Int,
976+
lineBreakWordStyle: Int,
966977
alignment: Layout.Alignment,
967978
justificationMode: Int,
968979
ellipsizeMode: TextUtils.TruncateAt?,
@@ -990,6 +1001,21 @@ internal object TextLayoutManager {
9901001
builder.setUseLineSpacingFromFallbacks(true)
9911002
}
9921003

1004+
// Leave the platform default in place unless a style was requested, so text without these props
1005+
// lays out exactly as before.
1006+
if (
1007+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
1008+
(lineBreakStyle != LineBreakConfig.LINE_BREAK_STYLE_NONE ||
1009+
lineBreakWordStyle != LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE)
1010+
) {
1011+
builder.setLineBreakConfig(
1012+
LineBreakConfig.Builder()
1013+
.setLineBreakStyle(lineBreakStyle)
1014+
.setLineBreakWordStyle(lineBreakWordStyle)
1015+
.build()
1016+
)
1017+
}
1018+
9931019
// Android shifts negative (left-side) start overhang itself. RTL start overhang is on the
9941020
// right, so createLayout reserves that space in a second pass while preserving the EXACT Yoga
9951021
// measurement returned to the caller.
@@ -1169,6 +1195,18 @@ internal object TextLayoutManager {
11691195
TextAttributeProps.getHyphenationFrequency(
11701196
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY),
11711197
)
1198+
val lineBreakStyle =
1199+
TextAttributeProps.getLineBreakStyle(
1200+
if (paragraphAttributes.contains(PA_KEY_LINE_BREAK_STYLE))
1201+
paragraphAttributes.getString(PA_KEY_LINE_BREAK_STYLE)
1202+
else null,
1203+
)
1204+
val lineBreakWordStyle =
1205+
TextAttributeProps.getLineBreakWordStyle(
1206+
if (paragraphAttributes.contains(PA_KEY_LINE_BREAK_WORD_STYLE))
1207+
paragraphAttributes.getString(PA_KEY_LINE_BREAK_WORD_STYLE)
1208+
else null,
1209+
)
11721210
val adjustFontSizeToFit =
11731211
if (paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT))
11741212
paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
@@ -1211,6 +1249,8 @@ internal object TextLayoutManager {
12111249
includeFontPadding,
12121250
textBreakStrategy,
12131251
hyphenationFrequency,
1252+
lineBreakStyle,
1253+
lineBreakWordStyle,
12141254
alignment,
12151255
justificationMode,
12161256
paint,
@@ -1226,6 +1266,8 @@ internal object TextLayoutManager {
12261266
includeFontPadding,
12271267
textBreakStrategy,
12281268
hyphenationFrequency,
1269+
lineBreakStyle,
1270+
lineBreakWordStyle,
12291271
alignment,
12301272
justificationMode,
12311273
ellipsizeMode,
@@ -1249,6 +1291,8 @@ internal object TextLayoutManager {
12491291
includeFontPadding,
12501292
textBreakStrategy,
12511293
hyphenationFrequency,
1294+
lineBreakStyle,
1295+
lineBreakWordStyle,
12521296
alignment,
12531297
justificationMode,
12541298
ellipsizeMode,
@@ -1372,6 +1416,8 @@ internal object TextLayoutManager {
13721416
includeFontPadding: Boolean,
13731417
textBreakStrategy: Int,
13741418
hyphenationFrequency: Int,
1419+
lineBreakStyle: Int,
1420+
lineBreakWordStyle: Int,
13751421
alignment: Layout.Alignment,
13761422
justificationMode: Int,
13771423
paint: TextPaint,
@@ -1436,6 +1482,8 @@ internal object TextLayoutManager {
14361482
includeFontPadding,
14371483
textBreakStrategy,
14381484
hyphenationFrequency,
1485+
lineBreakStyle,
1486+
lineBreakWordStyle,
14391487
alignment,
14401488
justificationMode,
14411489
null,

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest.kt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.facebook.react.views.text
99

1010
import android.annotation.SuppressLint
11+
import android.graphics.text.LineBreakConfig
1112
import android.text.BoringLayout
1213
import android.text.Layout
1314
import android.text.SpannableString
@@ -104,6 +105,8 @@ class TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest {
104105
java.lang.Boolean.TYPE,
105106
java.lang.Integer.TYPE,
106107
java.lang.Integer.TYPE,
108+
java.lang.Integer.TYPE,
109+
java.lang.Integer.TYPE,
107110
Layout.Alignment::class.java,
108111
java.lang.Integer.TYPE,
109112
TextUtils.TruncateAt::class.java,
@@ -121,6 +124,8 @@ class TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest {
121124
/* includeFontPadding = */ false,
122125
/* textBreakStrategy = */ Layout.BREAK_STRATEGY_HIGH_QUALITY,
123126
/* hyphenationFrequency = */ Layout.HYPHENATION_FREQUENCY_NONE,
127+
/* lineBreakStyle = */ LineBreakConfig.LINE_BREAK_STYLE_NONE,
128+
/* lineBreakWordStyle = */ LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE,
124129
Layout.Alignment.ALIGN_NORMAL,
125130
/* justificationMode = */ 0,
126131
/* ellipsizeMode = */ null,

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerMinimumFontScaleTest.kt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.facebook.react.views.text
99

10+
import android.graphics.text.LineBreakConfig
1011
import android.text.Layout
1112
import android.text.SpannableString
1213
import android.text.Spanned
@@ -123,6 +124,8 @@ class TextLayoutManagerMinimumFontScaleTest {
123124
true,
124125
Layout.BREAK_STRATEGY_SIMPLE,
125126
Layout.HYPHENATION_FREQUENCY_NONE,
127+
/* lineBreakStyle = */ LineBreakConfig.LINE_BREAK_STYLE_NONE,
128+
/* lineBreakWordStyle = */ LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE,
126129
Layout.Alignment.ALIGN_NORMAL,
127130
0,
128131
newPaint(),
@@ -149,6 +152,8 @@ class TextLayoutManagerMinimumFontScaleTest {
149152
true,
150153
Layout.BREAK_STRATEGY_SIMPLE,
151154
Layout.HYPHENATION_FREQUENCY_NONE,
155+
/* lineBreakStyle = */ LineBreakConfig.LINE_BREAK_STYLE_NONE,
156+
/* lineBreakWordStyle = */ LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE,
152157
Layout.Alignment.ALIGN_NORMAL,
153158
0,
154159
newPaint(),

0 commit comments

Comments
 (0)