Skip to content

Commit 1c240ae

Browse files
Mehdi Mulanifacebook-github-bot
Mehdi Mulani
authored andcommittedOct 17, 2018
Fix onTextLayout metrics on Android when using alignText
Summary: With this, we send the correct x position when using center or right aligned text. In order to accomplish this though, we have to pass the text alignment into the Layout object that we create. Also update RNTester to allow us to try different alignments. Reviewed By: sahrens Differential Revision: D10316494 fbshipit-source-id: 11c7d2a59e636528f12211168acb46f16b54a126
1 parent b951499 commit 1c240ae

File tree

4 files changed

+93
-24
lines changed

4 files changed

+93
-24
lines changed
 

‎RNTester/js/Shared/TextLegend.js

+49-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class TextLegend extends React.Component<*, *> {
1717
state = {
1818
textMetrics: [],
1919
language: 'english',
20+
alignment: 'left',
21+
fontSize: 50,
2022
};
2123

2224
render() {
@@ -50,6 +52,18 @@ class TextLegend extends React.Component<*, *> {
5052
};
5153
return (
5254
<View>
55+
<Text
56+
onPress={() =>
57+
this.setState(prevState => ({fontSize: prevState.fontSize + 3}))
58+
}>
59+
Increase size
60+
</Text>
61+
<Text
62+
onPress={() =>
63+
this.setState(prevState => ({fontSize: prevState.fontSize - 3}))
64+
}>
65+
Decrease size
66+
</Text>
5367
<Picker
5468
selectedValue={this.state.language}
5569
onValueChange={itemValue => this.setState({language: itemValue})}>
@@ -179,17 +193,48 @@ class TextLegend extends React.Component<*, *> {
179193
}}>
180194
End of text
181195
</Text>,
196+
<View
197+
key="start of text view"
198+
style={{
199+
top: y,
200+
height: height,
201+
width: 1,
202+
left: x,
203+
position: 'absolute',
204+
backgroundColor: 'brown',
205+
}}
206+
/>,
207+
<Text
208+
key="start of text text"
209+
style={{
210+
top: y,
211+
left: x + 5,
212+
position: 'absolute',
213+
color: 'brown',
214+
}}>
215+
Start of text
216+
</Text>,
182217
];
183218
},
184219
)}
185220
<Text
186-
onTextLayout={event =>
187-
this.setState({textMetrics: event.nativeEvent.lines})
188-
}
189-
style={{fontSize: 50}}>
221+
onTextLayout={event => {
222+
this.setState({textMetrics: event.nativeEvent.lines});
223+
}}
224+
style={{
225+
fontSize: this.state.fontSize,
226+
textAlign: this.state.alignment,
227+
}}>
190228
{PANGRAMS[this.state.language]}
191229
</Text>
192230
</View>
231+
<Picker
232+
selectedValue={this.state.alignment}
233+
onValueChange={itemValue => this.setState({alignment: itemValue})}>
234+
<Picker.Item label="Left align" value="left" />
235+
<Picker.Item label="Center align" value="center" />
236+
<Picker.Item label="Right align" value="right" />
237+
</Picker>
193238
</View>
194239
);
195240
}

‎ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java

+19-11
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,38 @@
1717
import com.facebook.react.bridge.WritableMap;
1818

1919
public class FontMetricsUtil {
20+
21+
private static final String CAP_HEIGHT_MEASUREMENT_TEXT = "T";
22+
private static final String X_HEIGHT_MEASUREMENT_TEXT = "x";
23+
private static final float AMPLIFICATION_FACTOR = 100;
24+
2025
public static WritableArray getFontMetrics(CharSequence text, Layout layout, TextPaint paint, Context context) {
2126
DisplayMetrics dm = context.getResources().getDisplayMetrics();
2227
WritableArray lines = Arguments.createArray();
28+
// To calculate xHeight and capHeight we have to render an "x" and "T" and manually measure their height.
29+
// In order to get more precision than Android offers, we blow up the text size by 100 and measure it.
30+
// Luckily, text size affects rendering linearly, so we can do this trick.
31+
TextPaint paintCopy = new TextPaint(paint);
32+
paintCopy.setTextSize(paintCopy.getTextSize() * AMPLIFICATION_FACTOR);
33+
Rect capHeightBounds = new Rect();
34+
paintCopy.getTextBounds(CAP_HEIGHT_MEASUREMENT_TEXT, 0, CAP_HEIGHT_MEASUREMENT_TEXT.length(), capHeightBounds);
35+
double capHeight = capHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density;
36+
Rect xHeightBounds = new Rect();
37+
paintCopy.getTextBounds(X_HEIGHT_MEASUREMENT_TEXT, 0, X_HEIGHT_MEASUREMENT_TEXT.length(), xHeightBounds);
38+
double xHeight = xHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density;
2339
for (int i = 0; i < layout.getLineCount(); i++) {
2440
Rect bounds = new Rect();
2541
layout.getLineBounds(i, bounds);
26-
2742
WritableMap line = Arguments.createMap();
28-
TextPaint paintCopy = new TextPaint(paint);
29-
paintCopy.setTextSize(paintCopy.getTextSize() * 100);
30-
Rect capHeightBounds = new Rect();
31-
paintCopy.getTextBounds("T", 0, 1, capHeightBounds);
32-
Rect xHeightBounds = new Rect();
33-
paintCopy.getTextBounds("x", 0, 1, xHeightBounds);
34-
line.putDouble("x", bounds.left / dm.density);
43+
line.putDouble("x", layout.getLineLeft(i) / dm.density);
3544
line.putDouble("y", bounds.top / dm.density);
3645
line.putDouble("width", layout.getLineWidth(i) / dm.density);
3746
line.putDouble("height", bounds.height() / dm.density);
3847
line.putDouble("descender", layout.getLineDescent(i) / dm.density);
3948
line.putDouble("ascender", -layout.getLineAscent(i) / dm.density);
4049
line.putDouble("baseline", layout.getLineBaseline(i) / dm.density);
41-
line.putDouble(
42-
"capHeight", capHeightBounds.height() / 100 * paint.getTextSize() / dm.density);
43-
line.putDouble("xHeight", xHeightBounds.height() / 100 * paint.getTextSize() / dm.density);
50+
line.putDouble("capHeight", capHeight);
51+
line.putDouble("xHeight", xHeight);
4452
line.putString(
4553
"text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString());
4654
lines.pushMap(line);

‎ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ private static void buildSpannedFromShadowNode(
181181
}
182182
}
183183

184+
protected int getDefaultFontSize() {
185+
return mAllowFontScaling ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
186+
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
187+
}
188+
184189
protected static Spannable spannedFromShadowNode(
185190
ReactBaseTextShadowNode textShadowNode, String text) {
186191
SpannableStringBuilder sb = new SpannableStringBuilder();
@@ -199,10 +204,7 @@ protected static Spannable spannedFromShadowNode(
199204
}
200205

201206
if (textShadowNode.mFontSize == UNSET) {
202-
int defaultFontSize =
203-
textShadowNode.mAllowFontScaling
204-
? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
205-
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
207+
int defaultFontSize = textShadowNode.getDefaultFontSize();
206208

207209
ops.add(new SetSpanOperation(0, sb.length(), new AbsoluteSizeSpan(defaultFontSize)));
208210
}

‎ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java

+19-5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public long measure(
6464
YogaMeasureMode heightMode) {
6565
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
6666
TextPaint textPaint = sTextPaintInstance;
67+
textPaint.setTextSize(mFontSize != UNSET ? mFontSize : getDefaultFontSize());
6768
Layout layout;
6869
Spanned text = Assertions.assertNotNull(
6970
mPreparedSpannableText,
@@ -75,6 +76,19 @@ public long measure(
7576
// technically, width should never be negative, but there is currently a bug in
7677
boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;
7778

79+
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
80+
switch (getTextAlign()) {
81+
case Gravity.LEFT:
82+
alignment = Layout.Alignment.ALIGN_NORMAL;
83+
break;
84+
case Gravity.RIGHT:
85+
alignment = Layout.Alignment.ALIGN_OPPOSITE;
86+
break;
87+
case Gravity.CENTER_HORIZONTAL:
88+
alignment = Layout.Alignment.ALIGN_CENTER;
89+
break;
90+
}
91+
7892
if (boring == null &&
7993
(unconstrainedWidth ||
8094
(!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
@@ -87,13 +101,13 @@ public long measure(
87101
text,
88102
textPaint,
89103
hintWidth,
90-
Layout.Alignment.ALIGN_NORMAL,
104+
alignment,
91105
1.f,
92106
0.f,
93107
mIncludeFontPadding);
94108
} else {
95109
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
96-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
110+
.setAlignment(alignment)
97111
.setLineSpacing(0.f, 1.f)
98112
.setIncludePad(mIncludeFontPadding)
99113
.setBreakStrategy(mTextBreakStrategy)
@@ -108,7 +122,7 @@ public long measure(
108122
text,
109123
textPaint,
110124
boring.width,
111-
Layout.Alignment.ALIGN_NORMAL,
125+
alignment,
112126
1.f,
113127
0.f,
114128
boring,
@@ -121,13 +135,13 @@ public long measure(
121135
text,
122136
textPaint,
123137
(int) width,
124-
Layout.Alignment.ALIGN_NORMAL,
138+
alignment,
125139
1.f,
126140
0.f,
127141
mIncludeFontPadding);
128142
} else {
129143
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
130-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
144+
.setAlignment(alignment)
131145
.setLineSpacing(0.f, 1.f)
132146
.setIncludePad(mIncludeFontPadding)
133147
.setBreakStrategy(mTextBreakStrategy)

0 commit comments

Comments
 (0)
Please sign in to comment.