Skip to content

Commit d4fff24

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 deef8f4 commit d4fff24

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;
@@ -194,6 +197,10 @@ private void initView() {
194197
}
195198

196199
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
200+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
201+
setLineBreakStyle(LineBreakConfig.LINE_BREAK_STYLE_NONE);
202+
setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE);
203+
}
197204
updateView(); // call after changing ellipsizeLocation in particular
198205
}
199206

@@ -246,6 +253,8 @@ protected void onDraw(Canvas canvas) {
246253
getIncludeFontPadding(),
247254
getBreakStrategy(),
248255
getHyphenationFrequency(),
256+
getLineBreakStyleCompat(),
257+
getLineBreakWordStyleCompat(),
249258
// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
250259
// how exactly lines are aligned, just their width
251260
Layout.Alignment.ALIGN_NORMAL,
@@ -575,6 +584,34 @@ public void setHyphenationFrequency(int hyphenationFrequency) {
575584
mShouldAdjustSpannableFontSize = true;
576585
}
577586

587+
@Override
588+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
589+
public void setLineBreakStyle(int lineBreakStyle) {
590+
super.setLineBreakStyle(lineBreakStyle);
591+
mShouldAdjustSpannableFontSize = true;
592+
}
593+
594+
@Override
595+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
596+
public void setLineBreakWordStyle(int lineBreakWordStyle) {
597+
super.setLineBreakWordStyle(lineBreakWordStyle);
598+
mShouldAdjustSpannableFontSize = true;
599+
}
600+
601+
@SuppressLint("InlinedApi")
602+
private int getLineBreakStyleCompat() {
603+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
604+
? getLineBreakStyle()
605+
: LineBreakConfig.LINE_BREAK_STYLE_NONE;
606+
}
607+
608+
@SuppressLint("InlinedApi")
609+
private int getLineBreakWordStyleCompat() {
610+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
611+
? getLineBreakWordStyle()
612+
: LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE;
613+
}
614+
578615
@Override
579616
public void setLetterSpacing(float letterSpacing) {
580617
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
@@ -339,6 +339,20 @@ public constructor(
339339
}
340340
}
341341

342+
@ReactProp(name = "android_lineBreakStyle")
343+
public fun setAndroidLineBreakStyle(view: ReactTextView, lineBreakStyle: String?) {
344+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
345+
view.lineBreakStyle = TextAttributeProps.getLineBreakStyle(lineBreakStyle)
346+
}
347+
}
348+
349+
@ReactProp(name = "android_lineBreakWordStyle")
350+
public fun setAndroidLineBreakWordStyle(view: ReactTextView, lineBreakWordStyle: String?) {
351+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
352+
view.lineBreakWordStyle = TextAttributeProps.getLineBreakWordStyle(lineBreakWordStyle)
353+
}
354+
}
355+
342356
@ReactPropGroup(
343357
names =
344358
[

‎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
@@ -608,6 +610,34 @@ public class TextAttributeProps private constructor() {
608610
else -> Layout.HYPHENATION_FREQUENCY_FULL
609611
}
610612

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

@@ -847,6 +850,8 @@ internal object TextLayoutManager {
847850
includeFontPadding: Boolean,
848851
textBreakStrategy: Int,
849852
hyphenationFrequency: Int,
853+
lineBreakStyle: Int,
854+
lineBreakWordStyle: Int,
850855
alignment: Layout.Alignment,
851856
justificationMode: Int,
852857
ellipsizeMode: TextUtils.TruncateAt?,
@@ -893,6 +898,8 @@ internal object TextLayoutManager {
893898
includeFontPadding,
894899
textBreakStrategy,
895900
hyphenationFrequency,
901+
lineBreakStyle,
902+
lineBreakWordStyle,
896903
alignment,
897904
justificationMode,
898905
ellipsizeMode,
@@ -911,6 +918,8 @@ internal object TextLayoutManager {
911918
includeFontPadding,
912919
textBreakStrategy,
913920
hyphenationFrequency,
921+
lineBreakStyle,
922+
lineBreakWordStyle,
914923
alignment,
915924
justificationMode,
916925
ellipsizeMode,
@@ -961,6 +970,8 @@ internal object TextLayoutManager {
961970
includeFontPadding: Boolean,
962971
textBreakStrategy: Int,
963972
hyphenationFrequency: Int,
973+
lineBreakStyle: Int,
974+
lineBreakWordStyle: Int,
964975
alignment: Layout.Alignment,
965976
justificationMode: Int,
966977
ellipsizeMode: TextUtils.TruncateAt?,
@@ -988,6 +999,21 @@ internal object TextLayoutManager {
988999
builder.setUseLineSpacingFromFallbacks(true)
9891000
}
9901001

1002+
// Leave the platform default in place unless a style was requested, so text without these props
1003+
// lays out exactly as before.
1004+
if (
1005+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
1006+
(lineBreakStyle != LineBreakConfig.LINE_BREAK_STYLE_NONE ||
1007+
lineBreakWordStyle != LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE)
1008+
) {
1009+
builder.setLineBreakConfig(
1010+
LineBreakConfig.Builder()
1011+
.setLineBreakStyle(lineBreakStyle)
1012+
.setLineBreakWordStyle(lineBreakWordStyle)
1013+
.build()
1014+
)
1015+
}
1016+
9911017
// Android shifts negative (left-side) start overhang itself. RTL start overhang is on the
9921018
// right, so createLayout reserves that space in a second pass while preserving the EXACT Yoga
9931019
// measurement returned to the caller.
@@ -1147,6 +1173,18 @@ internal object TextLayoutManager {
11471173
TextAttributeProps.getHyphenationFrequency(
11481174
paragraphAttributes.getString(PA_KEY_HYPHENATION_FREQUENCY),
11491175
)
1176+
val lineBreakStyle =
1177+
TextAttributeProps.getLineBreakStyle(
1178+
if (paragraphAttributes.contains(PA_KEY_LINE_BREAK_STYLE))
1179+
paragraphAttributes.getString(PA_KEY_LINE_BREAK_STYLE)
1180+
else null,
1181+
)
1182+
val lineBreakWordStyle =
1183+
TextAttributeProps.getLineBreakWordStyle(
1184+
if (paragraphAttributes.contains(PA_KEY_LINE_BREAK_WORD_STYLE))
1185+
paragraphAttributes.getString(PA_KEY_LINE_BREAK_WORD_STYLE)
1186+
else null,
1187+
)
11501188
val adjustFontSizeToFit =
11511189
if (paragraphAttributes.contains(PA_KEY_ADJUST_FONT_SIZE_TO_FIT))
11521190
paragraphAttributes.getBoolean(PA_KEY_ADJUST_FONT_SIZE_TO_FIT)
@@ -1189,6 +1227,8 @@ internal object TextLayoutManager {
11891227
includeFontPadding,
11901228
textBreakStrategy,
11911229
hyphenationFrequency,
1230+
lineBreakStyle,
1231+
lineBreakWordStyle,
11921232
alignment,
11931233
justificationMode,
11941234
paint,
@@ -1204,6 +1244,8 @@ internal object TextLayoutManager {
12041244
includeFontPadding,
12051245
textBreakStrategy,
12061246
hyphenationFrequency,
1247+
lineBreakStyle,
1248+
lineBreakWordStyle,
12071249
alignment,
12081250
justificationMode,
12091251
ellipsizeMode,
@@ -1227,6 +1269,8 @@ internal object TextLayoutManager {
12271269
includeFontPadding,
12281270
textBreakStrategy,
12291271
hyphenationFrequency,
1272+
lineBreakStyle,
1273+
lineBreakWordStyle,
12301274
alignment,
12311275
justificationMode,
12321276
ellipsizeMode,
@@ -1349,6 +1393,8 @@ internal object TextLayoutManager {
13491393
includeFontPadding: Boolean,
13501394
textBreakStrategy: Int,
13511395
hyphenationFrequency: Int,
1396+
lineBreakStyle: Int,
1397+
lineBreakWordStyle: Int,
13521398
alignment: Layout.Alignment,
13531399
justificationMode: Int,
13541400
paint: TextPaint,
@@ -1413,6 +1459,8 @@ internal object TextLayoutManager {
14131459
includeFontPadding,
14141460
textBreakStrategy,
14151461
hyphenationFrequency,
1462+
lineBreakStyle,
1463+
lineBreakWordStyle,
14161464
alignment,
14171465
justificationMode,
14181466
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)