Skip to content

Commit fd79fc4

Browse files
strindhaugguyca
authored andcommitted
Fix: incorrect line-height calculation
Summary: There seems to be a rounding error in the android code for line height, so that for some fonts and at some combinations of line height and font size the actual height of the elements seems to be slightly too short. I've identified one issue that I mentioned here facebook#10712 (comment) that could at least explain some of the problem. That when the line-height minus the original sum of the absolute value of top and bottom from the metrics, happens to be an odd number, the division by two causes a rounding error of 1, so that the actual line height is 1pt less than it should. The fix uses floating point division instead of integer division, and rounds (arbitrarily) the negative values up and the positive values down so that the total is still the correct for odd numbers. It turns out that only ascent and descent is used to give the actual line-height between lines in the same text-element. The top and bottom values are only used for padding the top and bottom of the text. So when the line-height is greater than the font size and the extra padding this PR sets the ascent and descent to the same value as the top and bottom respectively. I've renamed the shouldIncreaseAllMetricsProportionally test to evenLineHeightShouldIncreaseAllMetricsProportionally and added an extra assertion to check that bottom-top still equals the line height. Added another test oddLineHeightShouldAlsoWork that is similar but uses an odd number for the line height to test that it still works with odd numbers. This test only uses the sum of the values so that it's indifferent to what value the implementation chooses to round up or down. Improvement on facebook#16448 Fix line-height calculation on Android. | Before | After | | ------------- |-------------| | ![without fix](https://user-images.githubusercontent.com/2144849/36150230-4404a0cc-10c3-11e8-8880-4ab84339c741.png) | ![actual fix](https://user-images.githubusercontent.com/2144849/36156620-eb496d0e-10d7-11e8-8bd1-1cb536a38fbf.png) | (All three columns have font size 16 and lineHeight: 32. The first one is has fixed height 9*32, the second is 9 Text elements, the last is one text element with lots of text limited to 9 lines, so they should be the same height. ) Closes facebook#17952 Differential Revision: D6980333 Pulled By: hramos fbshipit-source-id: 0a501358cfbf7f139fca46056d0d972b1daf6ae3
1 parent 2edc0ee commit fd79fc4

File tree

2 files changed

+113
-2
lines changed

2 files changed

+113
-2
lines changed

ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLineHeightSpan.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ public void chooseHeight(
4747
} else {
4848
// Show proportionally additional ascent and top
4949
final int additional = mHeight - (-fm.top + fm.bottom);
50-
fm.top -= additional;
51-
fm.ascent -= additional;
50+
51+
// Round up for the negative values and down for the positive values (arbritary choice)
52+
// So that bottom - top equals additional even if it's an odd number.
53+
fm.top -= Math.ceil(additional / 2.0f);
54+
fm.bottom += Math.floor(additional / 2.0f);
55+
fm.ascent = fm.top;
56+
fm.descent = fm.bottom;
5257
}
5358
}
5459
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.facebook.react.views.text;
2+
3+
import android.graphics.Paint;
4+
5+
import static org.fest.assertions.api.Assertions.assertThat;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
9+
import org.robolectric.RobolectricTestRunner;
10+
11+
@RunWith(RobolectricTestRunner.class)
12+
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
13+
public class CustomLineHeightSpanTest {
14+
15+
@Test
16+
public void evenLineHeightShouldIncreaseAllMetricsProportionally() {
17+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(22);
18+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
19+
fm.top = -10;
20+
fm.ascent = -5;
21+
fm.descent = 5;
22+
fm.bottom = 10;
23+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
24+
// Since line height is even it should be equally added to top and bottom.
25+
assertThat(fm.top).isEqualTo(-11);
26+
assertThat(fm.ascent).isEqualTo(-11);
27+
assertThat(fm.descent).isEqualTo(11);
28+
assertThat(fm.bottom).isEqualTo(11);
29+
assertThat(fm.bottom - fm.top).isEqualTo(22);
30+
}
31+
32+
@Test
33+
public void oddLineHeightShouldAlsoWork() {
34+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(23);
35+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
36+
fm.top = -10;
37+
fm.ascent = -5;
38+
fm.descent = 5;
39+
fm.bottom = 10;
40+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
41+
// Only test that the sum is correct so the implementation
42+
// is free to add the odd value either on top or bottom.
43+
assertThat(fm.descent - fm.ascent).isEqualTo(23);
44+
assertThat(fm.bottom - fm.top).isEqualTo(23);
45+
}
46+
47+
@Test
48+
public void shouldReduceTopFirst() {
49+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(19);
50+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
51+
fm.top = -10;
52+
fm.ascent = -5;
53+
fm.descent = 5;
54+
fm.bottom = 10;
55+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
56+
assertThat(fm.top).isEqualTo(-9);
57+
assertThat(fm.ascent).isEqualTo(-5);
58+
assertThat(fm.descent).isEqualTo(5);
59+
assertThat(fm.bottom).isEqualTo(10);
60+
}
61+
62+
@Test
63+
public void shouldReduceBottomSecond() {
64+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(14);
65+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
66+
fm.top = -10;
67+
fm.ascent = -5;
68+
fm.descent = 5;
69+
fm.bottom = 10;
70+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
71+
assertThat(fm.top).isEqualTo(-5);
72+
assertThat(fm.ascent).isEqualTo(-5);
73+
assertThat(fm.descent).isEqualTo(5);
74+
assertThat(fm.bottom).isEqualTo(9);
75+
}
76+
77+
@Test
78+
public void shouldReduceAscentThird() {
79+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(9);
80+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
81+
fm.top = -10;
82+
fm.ascent = -5;
83+
fm.descent = 5;
84+
fm.bottom = 10;
85+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
86+
assertThat(fm.top).isEqualTo(-4);
87+
assertThat(fm.ascent).isEqualTo(-4);
88+
assertThat(fm.descent).isEqualTo(5);
89+
assertThat(fm.bottom).isEqualTo(5);
90+
}
91+
92+
@Test
93+
public void shouldReduceDescentLast() {
94+
CustomLineHeightSpan customLineHeightSpan = new CustomLineHeightSpan(4);
95+
Paint.FontMetricsInt fm = new Paint.FontMetricsInt();
96+
fm.top = -10;
97+
fm.ascent = -5;
98+
fm.descent = 5;
99+
fm.bottom = 10;
100+
customLineHeightSpan.chooseHeight("Hi", 0, 2, 0, 0, fm);
101+
assertThat(fm.top).isEqualTo(0);
102+
assertThat(fm.ascent).isEqualTo(0);
103+
assertThat(fm.descent).isEqualTo(4);
104+
assertThat(fm.bottom).isEqualTo(4);
105+
}
106+
}

0 commit comments

Comments
 (0)