Skip to content

Commit 357f29d

Browse files
committed
fix(android): prevent text descender clipping
1 parent 29ab78a commit 357f29d

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import android.graphics.Paint.FontMetricsInt
1111
import android.text.style.LineHeightSpan
1212
import kotlin.math.ceil
1313
import kotlin.math.floor
14+
import kotlin.math.max
15+
import kotlin.math.min
1416

1517
/**
1618
* Implements a [LineHeightSpan] which follows web-like behavior for line height, unlike
@@ -28,6 +30,9 @@ internal class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan {
2830
v: Int,
2931
fm: FontMetricsInt,
3032
) {
33+
val originalTop = fm.top
34+
val originalBottom = fm.bottom
35+
3136
// https://www.w3.org/TR/css-inline-3/#inline-height
3237
// When its computed line-height is not normal, its layout bounds are derived solely from
3338
// metrics of its first available font (ignoring glyphs from other fonts), and leading is used
@@ -47,10 +52,10 @@ internal class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan {
4752
// line boxes to overlap (to allow too large glyphs to be drawn outside them), so we do not
4853
// adjust the top/bottom of interior line-boxes.
4954
if (start == 0) {
50-
fm.top = fm.ascent
55+
fm.top = min(originalTop, fm.ascent)
5156
}
5257
if (end == text.length) {
53-
fm.bottom = fm.descent
58+
fm.bottom = max(originalBottom, fm.descent)
5459
}
5560
}
5661
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text.internal.span
9+
10+
import android.graphics.Paint
11+
import org.assertj.core.api.Assertions.assertThat
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.robolectric.RobolectricTestRunner
15+
16+
@RunWith(RobolectricTestRunner::class)
17+
class CustomLineHeightSpanTest {
18+
19+
@Test
20+
fun tightLineHeightDoesNotClipFirstOrLastLineFontBounds() {
21+
val span = CustomLineHeightSpan(16f)
22+
val fm =
23+
Paint.FontMetricsInt().apply {
24+
top = -18
25+
ascent = -14
26+
descent = 6
27+
bottom = 8
28+
}
29+
30+
span.chooseHeight("gjpqy", 0, 5, 0, 0, fm)
31+
32+
assertThat(fm.ascent).isEqualTo(-12)
33+
assertThat(fm.descent).isEqualTo(4)
34+
assertThat(fm.top).isEqualTo(-18)
35+
assertThat(fm.bottom).isEqualTo(8)
36+
}
37+
38+
@Test
39+
fun looseLineHeightStillExpandsFirstAndLastLineBounds() {
40+
val span = CustomLineHeightSpan(24f)
41+
val fm =
42+
Paint.FontMetricsInt().apply {
43+
top = -18
44+
ascent = -14
45+
descent = 6
46+
bottom = 8
47+
}
48+
49+
span.chooseHeight("gjpqy", 0, 5, 0, 0, fm)
50+
51+
assertThat(fm.ascent).isEqualTo(-16)
52+
assertThat(fm.descent).isEqualTo(8)
53+
assertThat(fm.top).isEqualTo(-18)
54+
assertThat(fm.bottom).isEqualTo(8)
55+
}
56+
}

packages/rn-tester/js/examples/Text/TextExample.android.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,19 @@ function LineHeightExample(props: {}): React.Node {
11191119
<RNTesterText style={{fontSize: 20}}>Continually</RNTesterText> expedite
11201120
magnetic potentialities rather than client-focused interfaces.
11211121
</RNTesterText>
1122+
<RNTesterText
1123+
style={[
1124+
{
1125+
fontSize: 24,
1126+
lineHeight: 24,
1127+
borderColor: 'black',
1128+
borderWidth: 1,
1129+
},
1130+
styles.wrappedText,
1131+
]}
1132+
testID="line-height-matches-font-size-descenders">
1133+
gjpqy
1134+
</RNTesterText>
11221135
<RNTesterText
11231136
style={[
11241137
{

0 commit comments

Comments
 (0)