Skip to content

Commit 737f937

Browse files
Mehdi Mulanifacebook-github-bot
Mehdi Mulani
authored andcommitted
Android: Send <Text> metrics in onTextLayout events
Summary: @public As we're doing in D9440914 (OSS 64a5253), 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
1 parent 36199d3 commit 737f937

File tree

3 files changed

+84
-5
lines changed

3 files changed

+84
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text;
9+
10+
import android.content.Context;
11+
import android.graphics.Rect;
12+
import android.text.Layout;
13+
import android.text.TextPaint;
14+
import android.util.DisplayMetrics;
15+
import com.facebook.react.bridge.Arguments;
16+
import com.facebook.react.bridge.WritableArray;
17+
import com.facebook.react.bridge.WritableMap;
18+
19+
public class FontMetricsUtil {
20+
public static WritableArray getFontMetrics(CharSequence text, Layout layout, TextPaint paint, Context context) {
21+
DisplayMetrics dm = context.getResources().getDisplayMetrics();
22+
WritableArray lines = Arguments.createArray();
23+
for (int i = 0; i < layout.getLineCount(); i++) {
24+
Rect bounds = new Rect();
25+
layout.getLineBounds(i, bounds);
26+
27+
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);
35+
line.putDouble("y", bounds.top / dm.density);
36+
line.putDouble("width", layout.getLineWidth(i) / dm.density);
37+
line.putDouble("height", bounds.height() / dm.density);
38+
line.putDouble("descender", layout.getLineDescent(i) / dm.density);
39+
line.putDouble("ascender", -layout.getLineAscent(i) / dm.density);
40+
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);
44+
line.putString(
45+
"text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString());
46+
lines.pushMap(line);
47+
}
48+
return lines;
49+
}
50+
}

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

+26-5
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,27 @@
77

88
package com.facebook.react.views.text;
99

10+
import android.graphics.Rect;
1011
import android.os.Build;
1112
import android.text.BoringLayout;
1213
import android.text.Layout;
1314
import android.text.Spannable;
1415
import android.text.Spanned;
1516
import android.text.StaticLayout;
1617
import android.text.TextPaint;
18+
import android.util.DisplayMetrics;
1719
import android.view.Gravity;
1820
import android.widget.TextView;
1921
import com.facebook.infer.annotation.Assertions;
22+
import com.facebook.react.bridge.Arguments;
23+
import com.facebook.react.bridge.WritableArray;
24+
import com.facebook.react.bridge.WritableMap;
2025
import com.facebook.react.uimanager.LayoutShadowNode;
2126
import com.facebook.react.uimanager.ReactShadowNodeImpl;
2227
import com.facebook.react.uimanager.Spacing;
2328
import com.facebook.react.uimanager.UIViewOperationQueue;
29+
import com.facebook.react.uimanager.annotations.ReactProp;
30+
import com.facebook.react.uimanager.events.RCTEventEmitter;
2431
import com.facebook.yoga.YogaConstants;
2532
import com.facebook.yoga.YogaDirection;
2633
import com.facebook.yoga.YogaMeasureFunction;
@@ -44,6 +51,8 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode {
4451

4552
private @Nullable Spannable mPreparedSpannableText;
4653

54+
private boolean mShouldNotifyOnTextLayout;
55+
4756
private final YogaMeasureFunction mTextMeasureFunction =
4857
new YogaMeasureFunction() {
4958
@Override
@@ -127,11 +136,18 @@ public long measure(
127136
}
128137
}
129138

130-
if (mNumberOfLines != UNSET &&
131-
mNumberOfLines < layout.getLineCount()) {
132-
return YogaMeasureOutput.make(
133-
layout.getWidth(),
134-
layout.getLineBottom(mNumberOfLines - 1));
139+
if (mShouldNotifyOnTextLayout) {
140+
WritableArray lines =
141+
FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, getThemedContext());
142+
WritableMap event = Arguments.createMap();
143+
event.putArray("lines", lines);
144+
getThemedContext()
145+
.getJSModule(RCTEventEmitter.class)
146+
.receiveEvent(getReactTag(), "topTextLayout", event);
147+
}
148+
149+
if (mNumberOfLines != UNSET && mNumberOfLines < layout.getLineCount()) {
150+
return YogaMeasureOutput.make(layout.getWidth(), layout.getLineBottom(mNumberOfLines - 1));
135151
} else {
136152
return YogaMeasureOutput.make(layout.getWidth(), layout.getHeight());
137153
}
@@ -223,4 +239,9 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
223239
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
224240
}
225241
}
242+
243+
@ReactProp(name = "onTextLayout")
244+
public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) {
245+
mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout;
246+
}
226247
}

ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java

+8
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
package com.facebook.react.views.text;
99

1010
import android.text.Spannable;
11+
import com.facebook.react.common.MapBuilder;
1112
import com.facebook.react.common.annotations.VisibleForTesting;
1213
import com.facebook.react.module.annotations.ReactModule;
1314
import com.facebook.react.uimanager.ThemedReactContext;
15+
import java.util.Map;
16+
import javax.annotation.Nullable;
1417

1518
/**
1619
* Concrete class for {@link ReactTextAnchorViewManager} which represents view managers of anchor
@@ -58,4 +61,9 @@ protected void onAfterUpdateTransaction(ReactTextView view) {
5861
super.onAfterUpdateTransaction(view);
5962
view.updateView();
6063
}
64+
65+
@Override
66+
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
67+
return MapBuilder.of("topTextLayout", MapBuilder.of("registrationName", "onTextLayout"));
68+
}
6169
}

0 commit comments

Comments
 (0)