From 1c240ae898e26534b8d9a09a334dec02e96faa05 Mon Sep 17 00:00:00 2001 From: Mehdi Mulani Date: Wed, 17 Oct 2018 12:48:19 -0700 Subject: [PATCH] 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 --- RNTester/js/Shared/TextLegend.js | 53 +++++++++++++++++-- .../react/views/text/FontMetricsUtil.java | 30 +++++++---- .../views/text/ReactBaseTextShadowNode.java | 10 ++-- .../react/views/text/ReactTextShadowNode.java | 24 +++++++-- 4 files changed, 93 insertions(+), 24 deletions(-) diff --git a/RNTester/js/Shared/TextLegend.js b/RNTester/js/Shared/TextLegend.js index 3708ddeaf0c14a..56cf17607c6b64 100644 --- a/RNTester/js/Shared/TextLegend.js +++ b/RNTester/js/Shared/TextLegend.js @@ -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 ( + + this.setState(prevState => ({fontSize: prevState.fontSize + 3})) + }> + Increase size + + + this.setState(prevState => ({fontSize: prevState.fontSize - 3})) + }> + Decrease size + this.setState({language: itemValue})}> @@ -179,17 +193,48 @@ class TextLegend extends React.Component<*, *> { }}> End of text , + , + + Start of text + , ]; }, )} - 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]} + this.setState({alignment: itemValue})}> + + + + ); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java index 2a235a355d4e21..419985323c3f2c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java @@ -17,30 +17,38 @@ 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); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index 6e6d2cfef8441b..b1c68b987e9cdb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -181,6 +181,11 @@ private static void buildSpannedFromShadowNode( } } + 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 @@ protected static Spannable spannedFromShadowNode( } 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))); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 14855f96820530..8b1ba78d131830 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -64,6 +64,7 @@ public long measure( 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 long measure( // 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 long measure( 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 long measure( text, textPaint, boring.width, - Layout.Alignment.ALIGN_NORMAL, + alignment, 1.f, 0.f, boring, @@ -121,13 +135,13 @@ public long measure( 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)