From 737f93705ca8b5d3fdd207f870cf27adcf1e885b Mon Sep 17 00:00:00 2001 From: Mehdi Mulani Date: Wed, 12 Sep 2018 14:13:47 -0700 Subject: [PATCH] Android: Send metrics in onTextLayout events Summary: @public As we're doing in D9440914 (OSS 64a52532fe88b482ae4b998133d340e6e1141a0f), send text metrics in an onTextLayout callback. These can be used by surrounding views for doing complicated layout like: - displaying a cursor at the end of text - vertical centering using capheight-baseline This right now isn't very performant but is only done when `onTextLayout` is set. I plan to optimize it with a capheight and xheight cache in a follow up diff. Reviewed By: achen1 Differential Revision: D9585613 fbshipit-source-id: aa20535b8371d5aecf15822d66a0d973c9a7eeda --- .../react/views/text/FontMetricsUtil.java | 50 +++++++++++++++++++ .../react/views/text/ReactTextShadowNode.java | 31 ++++++++++-- .../views/text/ReactTextViewManager.java | 8 +++ 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java 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 new file mode 100644 index 00000000000000..2a235a355d4e21 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.TextPaint; +import android.util.DisplayMetrics; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +public class FontMetricsUtil { + public static WritableArray getFontMetrics(CharSequence text, Layout layout, TextPaint paint, Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + WritableArray lines = Arguments.createArray(); + 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("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.putString( + "text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString()); + lines.pushMap(line); + } + return lines; + } +} 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 0ac46676bca255..c99a4becc64e24 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 @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import android.graphics.Rect; import android.os.Build; import android.text.BoringLayout; import android.text.Layout; @@ -14,13 +15,19 @@ import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; +import android.util.DisplayMetrics; import android.view.Gravity; import android.widget.TextView; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.ReactShadowNodeImpl; import com.facebook.react.uimanager.Spacing; import com.facebook.react.uimanager.UIViewOperationQueue; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaDirection; import com.facebook.yoga.YogaMeasureFunction; @@ -44,6 +51,8 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { private @Nullable Spannable mPreparedSpannableText; + private boolean mShouldNotifyOnTextLayout; + private final YogaMeasureFunction mTextMeasureFunction = new YogaMeasureFunction() { @Override @@ -127,11 +136,18 @@ public long measure( } } - if (mNumberOfLines != UNSET && - mNumberOfLines < layout.getLineCount()) { - return YogaMeasureOutput.make( - layout.getWidth(), - layout.getLineBottom(mNumberOfLines - 1)); + if (mShouldNotifyOnTextLayout) { + WritableArray lines = + FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, getThemedContext()); + WritableMap event = Arguments.createMap(); + event.putArray("lines", lines); + getThemedContext() + .getJSModule(RCTEventEmitter.class) + .receiveEvent(getReactTag(), "topTextLayout", event); + } + + if (mNumberOfLines != UNSET && mNumberOfLines < layout.getLineCount()) { + return YogaMeasureOutput.make(layout.getWidth(), layout.getLineBottom(mNumberOfLines - 1)); } else { return YogaMeasureOutput.make(layout.getWidth(), layout.getHeight()); } @@ -223,4 +239,9 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); } } + + @ReactProp(name = "onTextLayout") + public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) { + mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index c57dc634ce5a3e..7f63f2b20ac6ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -8,9 +8,12 @@ package com.facebook.react.views.text; import android.text.Spannable; +import com.facebook.react.common.MapBuilder; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.ThemedReactContext; +import java.util.Map; +import javax.annotation.Nullable; /** * Concrete class for {@link ReactTextAnchorViewManager} which represents view managers of anchor @@ -58,4 +61,9 @@ protected void onAfterUpdateTransaction(ReactTextView view) { super.onAfterUpdateTransaction(view); view.updateView(); } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of("topTextLayout", MapBuilder.of("registrationName", "onTextLayout")); + } }