Skip to content

Commit 7b6516e

Browse files
fkgozalimeta-codesync[bot]
authored andcommitted
Keep mixed-direction text on screen with Android 15 start overhang (#59167)
Summary: Pull Request resolved: #59167 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+ Reviewed By: Abbondanzo Differential Revision: D125908523 fbshipit-source-id: 9955f447b3ab0eda603ce88971d1dab59eddb9d1
1 parent ad2f451 commit 7b6516e

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
@@ -913,6 +913,24 @@ internal object TextLayoutManager {
913913
enableStartOverhang,
914914
)
915915

916+
// Android 15/16 leak RTL line bounds into later LTR lines, shifting the whole drawing right.
917+
if (enableStartOverhang && hasMixedParagraphDirections(layout)) {
918+
return buildLayout(
919+
text,
920+
layoutWidth,
921+
includeFontPadding,
922+
textBreakStrategy,
923+
hyphenationFrequency,
924+
alignment,
925+
justificationMode,
926+
ellipsizeMode,
927+
maxNumberOfLines,
928+
paint,
929+
enableStartOverhang,
930+
shiftDrawingOffsetForStartOverhang = false,
931+
)
932+
}
933+
916934
// Layout.draw shifts negative (left-side) overhang, but RTL line starts can overflow to the
917935
// right. Reserve that ink inside an EXACT layout without changing the width reported to Yoga.
918936
return if (enableStartOverhang) {
@@ -950,6 +968,15 @@ internal object TextLayoutManager {
950968
}
951969
}
952970

971+
@VisibleForTesting
972+
internal fun hasMixedParagraphDirections(layout: Layout): Boolean {
973+
if (Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM || layout.lineCount == 0) {
974+
return false
975+
}
976+
val firstDirection = layout.getParagraphDirection(0)
977+
return (1 until layout.lineCount).any { layout.getParagraphDirection(it) != firstDirection }
978+
}
979+
953980
@VisibleForTesting
954981
internal fun getRtlRightOverhang(layout: Layout): Int {
955982
if (
@@ -979,6 +1006,7 @@ internal object TextLayoutManager {
9791006
maxNumberOfLines: Int,
9801007
paint: TextPaint,
9811008
enableStartOverhang: Boolean,
1009+
shiftDrawingOffsetForStartOverhang: Boolean = enableStartOverhang,
9821010
): Layout {
9831011
val builder =
9841012
StaticLayout.Builder.obtain(text, 0, text.length, paint, layoutWidth)
@@ -1005,7 +1033,7 @@ internal object TextLayoutManager {
10051033
// measurement returned to the caller.
10061034
if (Build.VERSION.SDK_INT >= VERSION_CODE_VANILLA_ICE_CREAM) {
10071035
setUseBoundsForWidth(builder, enableStartOverhang)
1008-
setShiftDrawingOffsetForStartOverhang(builder, enableStartOverhang)
1036+
setShiftDrawingOffsetForStartOverhang(builder, shiftDrawingOffsetForStartOverhang)
10091037
}
10101038

10111039
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
@@ -115,6 +115,36 @@ class TextLayoutManagerStartOverhangTest {
115115
assertThat(layout).isSameAs(initialLayout)
116116
}
117117

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

0 commit comments

Comments
 (0)