Android: Send <Text> metrics in onTextLayout events

Summary:
@public
As we're doing in D9440914 (OSS 64a52532fe), 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
This commit is contained in:
Mehdi Mulani 2018-09-12 14:13:47 -07:00 committed by Facebook Github Bot
parent 36199d3dda
commit 737f93705c
3 changed files with 84 additions and 5 deletions

View File

@ -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;
}
}

View File

@ -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.Spannable;
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 class ReactTextShadowNode extends ReactBaseTextShadowNode {
}
}
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 class ReactTextShadowNode extends ReactBaseTextShadowNode {
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
}
}
@ReactProp(name = "onTextLayout")
public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) {
mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout;
}
}

View File

@ -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 @@ public class ReactTextViewManager
super.onAfterUpdateTransaction(view);
view.updateView();
}
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of("topTextLayout", MapBuilder.of("registrationName", "onTextLayout"));
}
}