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 коммит произвёл Facebook Github Bot
Родитель b9514995a2
Коммит 1c240ae898
4 изменённых файлов: 93 добавлений и 24 удалений

Просмотреть файл

@ -17,6 +17,8 @@ class TextLegend extends React.Component<*, *> {
state = { state = {
textMetrics: [], textMetrics: [],
language: 'english', language: 'english',
alignment: 'left',
fontSize: 50,
}; };
render() { render() {
@ -50,6 +52,18 @@ class TextLegend extends React.Component<*, *> {
}; };
return ( return (
<View> <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 <Picker
selectedValue={this.state.language} selectedValue={this.state.language}
onValueChange={itemValue => this.setState({language: itemValue})}> onValueChange={itemValue => this.setState({language: itemValue})}>
@ -179,17 +193,48 @@ class TextLegend extends React.Component<*, *> {
}}> }}>
End of text End of text
</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 <Text
onTextLayout={event => onTextLayout={event => {
this.setState({textMetrics: event.nativeEvent.lines}) this.setState({textMetrics: event.nativeEvent.lines});
} }}
style={{fontSize: 50}}> style={{
fontSize: this.state.fontSize,
textAlign: this.state.alignment,
}}>
{PANGRAMS[this.state.language]} {PANGRAMS[this.state.language]}
</Text> </Text>
</View> </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>
); );
} }

Просмотреть файл

@ -17,30 +17,38 @@ import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
public class FontMetricsUtil { 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) { public static WritableArray getFontMetrics(CharSequence text, Layout layout, TextPaint paint, Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics(); DisplayMetrics dm = context.getResources().getDisplayMetrics();
WritableArray lines = Arguments.createArray(); 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++) { for (int i = 0; i < layout.getLineCount(); i++) {
Rect bounds = new Rect(); Rect bounds = new Rect();
layout.getLineBounds(i, bounds); layout.getLineBounds(i, bounds);
WritableMap line = Arguments.createMap(); WritableMap line = Arguments.createMap();
TextPaint paintCopy = new TextPaint(paint); line.putDouble("x", layout.getLineLeft(i) / dm.density);
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("y", bounds.top / dm.density); line.putDouble("y", bounds.top / dm.density);
line.putDouble("width", layout.getLineWidth(i) / dm.density); line.putDouble("width", layout.getLineWidth(i) / dm.density);
line.putDouble("height", bounds.height() / dm.density); line.putDouble("height", bounds.height() / dm.density);
line.putDouble("descender", layout.getLineDescent(i) / dm.density); line.putDouble("descender", layout.getLineDescent(i) / dm.density);
line.putDouble("ascender", -layout.getLineAscent(i) / dm.density); line.putDouble("ascender", -layout.getLineAscent(i) / dm.density);
line.putDouble("baseline", layout.getLineBaseline(i) / dm.density); line.putDouble("baseline", layout.getLineBaseline(i) / dm.density);
line.putDouble( line.putDouble("capHeight", capHeight);
"capHeight", capHeightBounds.height() / 100 * paint.getTextSize() / dm.density); line.putDouble("xHeight", xHeight);
line.putDouble("xHeight", xHeightBounds.height() / 100 * paint.getTextSize() / dm.density);
line.putString( line.putString(
"text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString()); "text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString());
lines.pushMap(line); lines.pushMap(line);

Просмотреть файл

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

Просмотреть файл

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