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
This commit is contained in:
Mehdi Mulani
2018-10-17 12:48:19 -07:00
committed by Facebook Github Bot
parent b9514995a2
commit 1c240ae898
4 changed files with 93 additions and 24 deletions

View File

@@ -17,6 +17,8 @@ class TextLegend extends React.Component<*, *> {
state = {
textMetrics: [],
language: 'english',
alignment: 'left',
fontSize: 50,
};
render() {
@@ -50,6 +52,18 @@ class TextLegend extends React.Component<*, *> {
};
return (
<View>
<Text
onPress={() =>
this.setState(prevState => ({fontSize: prevState.fontSize + 3}))
}>
Increase size
</Text>
<Text
onPress={() =>
this.setState(prevState => ({fontSize: prevState.fontSize - 3}))
}>
Decrease size
</Text>
<Picker
selectedValue={this.state.language}
onValueChange={itemValue => this.setState({language: itemValue})}>
@@ -179,17 +193,48 @@ class TextLegend extends React.Component<*, *> {
}}>
End of text
</Text>,
<View
key="start of text view"
style={{
top: y,
height: height,
width: 1,
left: x,
position: 'absolute',
backgroundColor: 'brown',
}}
/>,
<Text
key="start of text text"
style={{
top: y,
left: x + 5,
position: 'absolute',
color: 'brown',
}}>
Start of text
</Text>,
];
},
)}
<Text
onTextLayout={event =>
this.setState({textMetrics: event.nativeEvent.lines})
}
style={{fontSize: 50}}>
onTextLayout={event => {
this.setState({textMetrics: event.nativeEvent.lines});
}}
style={{
fontSize: this.state.fontSize,
textAlign: this.state.alignment,
}}>
{PANGRAMS[this.state.language]}
</Text>
</View>
<Picker
selectedValue={this.state.alignment}
onValueChange={itemValue => this.setState({alignment: itemValue})}>
<Picker.Item label="Left align" value="left" />
<Picker.Item label="Center align" value="center" />
<Picker.Item label="Right align" value="right" />
</Picker>
</View>
);
}

View File

@@ -17,30 +17,38 @@ import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
public class FontMetricsUtil {
private static final String CAP_HEIGHT_MEASUREMENT_TEXT = "T";
private static final String X_HEIGHT_MEASUREMENT_TEXT = "x";
private static final float AMPLIFICATION_FACTOR = 100;
public static WritableArray getFontMetrics(CharSequence text, Layout layout, TextPaint paint, Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
WritableArray lines = Arguments.createArray();
// To calculate xHeight and capHeight we have to render an "x" and "T" and manually measure their height.
// In order to get more precision than Android offers, we blow up the text size by 100 and measure it.
// Luckily, text size affects rendering linearly, so we can do this trick.
TextPaint paintCopy = new TextPaint(paint);
paintCopy.setTextSize(paintCopy.getTextSize() * AMPLIFICATION_FACTOR);
Rect capHeightBounds = new Rect();
paintCopy.getTextBounds(CAP_HEIGHT_MEASUREMENT_TEXT, 0, CAP_HEIGHT_MEASUREMENT_TEXT.length(), capHeightBounds);
double capHeight = capHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density;
Rect xHeightBounds = new Rect();
paintCopy.getTextBounds(X_HEIGHT_MEASUREMENT_TEXT, 0, X_HEIGHT_MEASUREMENT_TEXT.length(), xHeightBounds);
double xHeight = xHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density;
for (int i = 0; i < layout.getLineCount(); i++) {
Rect bounds = new Rect();
layout.getLineBounds(i, bounds);
WritableMap line = Arguments.createMap();
TextPaint paintCopy = new TextPaint(paint);
paintCopy.setTextSize(paintCopy.getTextSize() * 100);
Rect capHeightBounds = new Rect();
paintCopy.getTextBounds("T", 0, 1, capHeightBounds);
Rect xHeightBounds = new Rect();
paintCopy.getTextBounds("x", 0, 1, xHeightBounds);
line.putDouble("x", bounds.left / dm.density);
line.putDouble("x", layout.getLineLeft(i) / dm.density);
line.putDouble("y", bounds.top / dm.density);
line.putDouble("width", layout.getLineWidth(i) / dm.density);
line.putDouble("height", bounds.height() / dm.density);
line.putDouble("descender", layout.getLineDescent(i) / dm.density);
line.putDouble("ascender", -layout.getLineAscent(i) / dm.density);
line.putDouble("baseline", layout.getLineBaseline(i) / dm.density);
line.putDouble(
"capHeight", capHeightBounds.height() / 100 * paint.getTextSize() / dm.density);
line.putDouble("xHeight", xHeightBounds.height() / 100 * paint.getTextSize() / dm.density);
line.putDouble("capHeight", capHeight);
line.putDouble("xHeight", xHeight);
line.putString(
"text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString());
lines.pushMap(line);

View File

@@ -181,6 +181,11 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
}
protected int getDefaultFontSize() {
return mAllowFontScaling ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
}
protected static Spannable spannedFromShadowNode(
ReactBaseTextShadowNode textShadowNode, String text) {
SpannableStringBuilder sb = new SpannableStringBuilder();
@@ -199,10 +204,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
if (textShadowNode.mFontSize == UNSET) {
int defaultFontSize =
textShadowNode.mAllowFontScaling
? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
int defaultFontSize = textShadowNode.getDefaultFontSize();
ops.add(new SetSpanOperation(0, sb.length(), new AbsoluteSizeSpan(defaultFontSize)));
}

View File

@@ -64,6 +64,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
YogaMeasureMode heightMode) {
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
TextPaint textPaint = sTextPaintInstance;
textPaint.setTextSize(mFontSize != UNSET ? mFontSize : getDefaultFontSize());
Layout layout;
Spanned text = Assertions.assertNotNull(
mPreparedSpannableText,
@@ -75,6 +76,19 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
// technically, width should never be negative, but there is currently a bug in
boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
switch (getTextAlign()) {
case Gravity.LEFT:
alignment = Layout.Alignment.ALIGN_NORMAL;
break;
case Gravity.RIGHT:
alignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
case Gravity.CENTER_HORIZONTAL:
alignment = Layout.Alignment.ALIGN_CENTER;
break;
}
if (boring == null &&
(unconstrainedWidth ||
(!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
@@ -87,13 +101,13 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
text,
textPaint,
hintWidth,
Layout.Alignment.ALIGN_NORMAL,
alignment,
1.f,
0.f,
mIncludeFontPadding);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setAlignment(alignment)
.setLineSpacing(0.f, 1.f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)
@@ -108,7 +122,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
text,
textPaint,
boring.width,
Layout.Alignment.ALIGN_NORMAL,
alignment,
1.f,
0.f,
boring,
@@ -121,13 +135,13 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
text,
textPaint,
(int) width,
Layout.Alignment.ALIGN_NORMAL,
alignment,
1.f,
0.f,
mIncludeFontPadding);
} else {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setAlignment(alignment)
.setLineSpacing(0.f, 1.f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)