Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,24 @@ internal object TextLayoutManager {
enableStartOverhang,
)

// Android 15/16 leak RTL line bounds into later LTR lines, shifting the whole drawing right.
if (enableStartOverhang && hasMixedParagraphDirections(layout)) {
return buildLayout(
text,
layoutWidth,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency,
alignment,
justificationMode,
ellipsizeMode,
maxNumberOfLines,
paint,
enableStartOverhang,
shiftDrawingOffsetForStartOverhang = false,
)
}

// Layout.draw shifts negative (left-side) overhang, but RTL line starts can overflow to the
// right. Reserve that ink inside an EXACT layout without changing the width reported to Yoga.
return if (enableStartOverhang) {
Expand Down Expand Up @@ -940,6 +958,15 @@ internal object TextLayoutManager {
}
}

@VisibleForTesting
internal fun hasMixedParagraphDirections(layout: Layout): Boolean {
if (Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM || layout.lineCount == 0) {
return false
}
val firstDirection = layout.getParagraphDirection(0)
return (1 until layout.lineCount).any { layout.getParagraphDirection(it) != firstDirection }
}

@VisibleForTesting
internal fun getRtlRightOverhang(layout: Layout): Int {
if (
Expand Down Expand Up @@ -969,6 +996,7 @@ internal object TextLayoutManager {
maxNumberOfLines: Int,
paint: TextPaint,
enableStartOverhang: Boolean,
shiftDrawingOffsetForStartOverhang: Boolean = enableStartOverhang,
): Layout {
val builder =
StaticLayout.Builder.obtain(text, 0, text.length, paint, layoutWidth)
Expand All @@ -995,7 +1023,7 @@ internal object TextLayoutManager {
// measurement returned to the caller.
if (Build.VERSION.SDK_INT >= VERSION_CODE_VANILLA_ICE_CREAM) {
setUseBoundsForWidth(builder, enableStartOverhang)
setShiftDrawingOffsetForStartOverhang(builder, enableStartOverhang)
setShiftDrawingOffsetForStartOverhang(builder, shiftDrawingOffsetForStartOverhang)
}

return builder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,36 @@ class TextLayoutManagerStartOverhangTest {
assertThat(layout).isSameAs(initialLayout)
}

@Test
@Config(sdk = [35])
fun `mixed paragraph directions disable the start overhang drawing shift`() {
// The blank line between the RTL paragraphs has no strong character, so it is laid out LTR.
val layout =
createLayout(
YogaMeasureMode.EXACTLY,
text = SpannableString("שלום\n\nעולם"),
maxNumberOfLines = 0,
)

assertThat(TextLayoutManager.hasMixedParagraphDirections(layout)).isTrue()
assertThat(getBooleanLayoutProperty(layout, "getUseBoundsForWidth")).isTrue()
assertThat(getBooleanLayoutProperty(layout, "getShiftDrawingOffsetForStartOverhang")).isFalse()
}

@Test
@Config(sdk = [35])
fun `single paragraph direction keeps the start overhang drawing shift`() {
val layout =
createLayout(
YogaMeasureMode.EXACTLY,
text = SpannableString("שלום\nעולם"),
maxNumberOfLines = 0,
)

assertThat(TextLayoutManager.hasMixedParagraphDirections(layout)).isFalse()
assertThat(getBooleanLayoutProperty(layout, "getShiftDrawingOffsetForStartOverhang")).isTrue()
}

@Test
@Config(sdk = [34])
fun `EXACTLY mode remains supported before Android 15`() {
Expand Down
Loading