Skip to content

Commit 92d6d0b

Browse files
fkgozalifacebook-github-bot
authored andcommitted
Keep mixed-direction text on screen with Android 15 start overhang
Summary: On Android 15 and 16.0, `Layout.computeDrawingBoundingBox()` reuses a single `RectF` for every line and `TextLine.metrics()` does not reset it. A line whose paragraph direction is LTR, laid out after an RTL line, inherits the RTL line's bounds in RTL-relative (negative) coordinates. With `setShiftDrawingOffsetForStartOverhang(true)`, `Layout.draw()` then translates the whole text right by about one line width, so the text disappears or only a few glyphs stay visible at the right edge. Common RTL content hits this: a blank line between Arabic or Hebrew paragraphs has no strong character and is laid out LTR. The platform fixed it in a later Android 16 release by resetting the rect per line. When an exactly sized layout has mixed paragraph directions, rebuild it without the start-overhang drawing shift. Bounds-based line breaking is kept, so the measured line breaks are unchanged. Single-direction text keeps the existing start-overhang behavior. Changelog: [Android][Fixed] - Fix multi-paragraph RTL `Text` with blank or LTR lines being drawn off-screen on Android 15+ Differential Revision: D125908523
1 parent 27e7721 commit 92d6d0b

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,24 @@ internal object TextLayoutManager {
903903
enableStartOverhang,
904904
)
905905

906+
// Android 15/16 leak RTL line bounds into later LTR lines, shifting the whole drawing right.
907+
if (enableStartOverhang && hasMixedParagraphDirections(layout)) {
908+
return buildLayout(
909+
text,
910+
layoutWidth,
911+
includeFontPadding,
912+
textBreakStrategy,
913+
hyphenationFrequency,
914+
alignment,
915+
justificationMode,
916+
ellipsizeMode,
917+
maxNumberOfLines,
918+
paint,
919+
enableStartOverhang,
920+
shiftDrawingOffsetForStartOverhang = false,
921+
)
922+
}
923+
906924
// Layout.draw shifts negative (left-side) overhang, but RTL line starts can overflow to the
907925
// right. Reserve that ink inside an EXACT layout without changing the width reported to Yoga.
908926
return if (enableStartOverhang) {
@@ -940,6 +958,15 @@ internal object TextLayoutManager {
940958
}
941959
}
942960

961+
@VisibleForTesting
962+
internal fun hasMixedParagraphDirections(layout: Layout): Boolean {
963+
if (Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM || layout.lineCount == 0) {
964+
return false
965+
}
966+
val firstDirection = layout.getParagraphDirection(0)
967+
return (1 until layout.lineCount).any { layout.getParagraphDirection(it) != firstDirection }
968+
}
969+
943970
@VisibleForTesting
944971
internal fun getRtlRightOverhang(layout: Layout): Int {
945972
if (
@@ -969,6 +996,7 @@ internal object TextLayoutManager {
969996
maxNumberOfLines: Int,
970997
paint: TextPaint,
971998
enableStartOverhang: Boolean,
999+
shiftDrawingOffsetForStartOverhang: Boolean = enableStartOverhang,
9721000
): Layout {
9731001
val builder =
9741002
StaticLayout.Builder.obtain(text, 0, text.length, paint, layoutWidth)
@@ -995,7 +1023,7 @@ internal object TextLayoutManager {
9951023
// measurement returned to the caller.
9961024
if (Build.VERSION.SDK_INT >= VERSION_CODE_VANILLA_ICE_CREAM) {
9971025
setUseBoundsForWidth(builder, enableStartOverhang)
998-
setShiftDrawingOffsetForStartOverhang(builder, enableStartOverhang)
1026+
setShiftDrawingOffsetForStartOverhang(builder, shiftDrawingOffsetForStartOverhang)
9991027
}
10001028

10011029
return builder.build()

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,36 @@ class TextLayoutManagerStartOverhangTest {
116116
assertThat(layout).isSameAs(initialLayout)
117117
}
118118

119+
@Test
120+
@Config(sdk = [35])
121+
fun `mixed paragraph directions disable the start overhang drawing shift`() {
122+
// The blank line between the RTL paragraphs has no strong character, so it is laid out LTR.
123+
val layout =
124+
createLayout(
125+
YogaMeasureMode.EXACTLY,
126+
text = SpannableString("שלום\n\nעולם"),
127+
maxNumberOfLines = 0,
128+
)
129+
130+
assertThat(TextLayoutManager.hasMixedParagraphDirections(layout)).isTrue()
131+
assertThat(getBooleanLayoutProperty(layout, "getUseBoundsForWidth")).isTrue()
132+
assertThat(getBooleanLayoutProperty(layout, "getShiftDrawingOffsetForStartOverhang")).isFalse()
133+
}
134+
135+
@Test
136+
@Config(sdk = [35])
137+
fun `single paragraph direction keeps the start overhang drawing shift`() {
138+
val layout =
139+
createLayout(
140+
YogaMeasureMode.EXACTLY,
141+
text = SpannableString("שלום\nעולם"),
142+
maxNumberOfLines = 0,
143+
)
144+
145+
assertThat(TextLayoutManager.hasMixedParagraphDirections(layout)).isFalse()
146+
assertThat(getBooleanLayoutProperty(layout, "getShiftDrawingOffsetForStartOverhang")).isTrue()
147+
}
148+
119149
@Test
120150
@Config(sdk = [34])
121151
fun `EXACTLY mode remains supported before Android 15`() {

0 commit comments

Comments
 (0)