mirror of
https://github.com/status-im/react-native.git
synced 2025-01-16 20:44:10 +00:00
Implement TextNodeRegion
Summary: NodeRegion is only able to describe a rectangular region for touch, which is not good enough for text, where we want to be able to assign different touch ids to individual words (and those can span more than one line and in general have non-rectangular structure). This diff adds TextNodeRegion which inserts additional markers into text Layout to allow individual words to have unique react tags. Reviewed By: ahmedre Differential Revision: D2757387
This commit is contained in:
parent
85cdfcd1f7
commit
381bf1b76f
@ -30,6 +30,10 @@ import android.text.Layout;
|
|||||||
mLayout = layout;
|
mLayout = layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Layout getLayout() {
|
||||||
|
return mLayout;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(FlatViewGroup parent, Canvas canvas) {
|
public void draw(FlatViewGroup parent, Canvas canvas) {
|
||||||
float left = getLeft();
|
float left = getLeft();
|
||||||
|
@ -171,14 +171,21 @@ import com.facebook.react.uimanager.ViewProps;
|
|||||||
mNodeRegions = nodeRegion;
|
mNodeRegions = nodeRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ final NodeRegion getNodeRegion() {
|
/* package */ void updateNodeRegion(float left, float top, float right, float bottom) {
|
||||||
return mNodeRegion;
|
if (mNodeRegion.mLeft != left || mNodeRegion.mTop != top ||
|
||||||
|
mNodeRegion.mRight != right || mNodeRegion.mBottom != bottom) {
|
||||||
|
setNodeRegion(new NodeRegion(left, top, right, bottom, getReactTag()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ final void setNodeRegion(NodeRegion nodeRegion) {
|
protected final void setNodeRegion(NodeRegion nodeRegion) {
|
||||||
mNodeRegion = nodeRegion;
|
mNodeRegion = nodeRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ final NodeRegion getNodeRegion() {
|
||||||
|
return mNodeRegion;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets boundaries of the View that this node maps to relative to the parent left/top coordinate.
|
* Sets boundaries of the View that this node maps to relative to the parent left/top coordinate.
|
||||||
*/
|
*/
|
||||||
|
@ -18,17 +18,9 @@ import com.facebook.react.uimanager.ReactShadowNode;
|
|||||||
*/
|
*/
|
||||||
/* package */ abstract class FlatTextShadowNode extends FlatShadowNode {
|
/* package */ abstract class FlatTextShadowNode extends FlatShadowNode {
|
||||||
|
|
||||||
/**
|
// these 2 are only used between collectText() and applySpans() calls.
|
||||||
* Recursively visits FlatTextShadowNode and its children,
|
private int mTextBegin;
|
||||||
* appending text to SpannableStringBuilder.
|
private int mTextEnd;
|
||||||
*/
|
|
||||||
protected abstract void collectText(SpannableStringBuilder builder);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively visits FlatTextShadowNode and its children,
|
|
||||||
* applying spans to SpannableStringBuilder.
|
|
||||||
*/
|
|
||||||
protected abstract void applySpans(SpannableStringBuilder builder);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagates changes up to RCTText without dirtying current node.
|
* Propagates changes up to RCTText without dirtying current node.
|
||||||
@ -44,4 +36,27 @@ import com.facebook.react.uimanager.ReactShadowNode;
|
|||||||
public boolean isVirtual() {
|
public boolean isVirtual() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively visits FlatTextShadowNode and its children,
|
||||||
|
* appending text to SpannableStringBuilder.
|
||||||
|
*/
|
||||||
|
/* package */ final void collectText(SpannableStringBuilder builder) {
|
||||||
|
mTextBegin = builder.length();
|
||||||
|
performCollectText(builder);
|
||||||
|
mTextEnd = builder.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively visits FlatTextShadowNode and its children,
|
||||||
|
* applying spans to SpannableStringBuilder.
|
||||||
|
*/
|
||||||
|
/* package */ final void applySpans(SpannableStringBuilder builder) {
|
||||||
|
if (mTextBegin != mTextEnd) {
|
||||||
|
performApplySpans(builder, mTextBegin, mTextEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void performCollectText(SpannableStringBuilder builder);
|
||||||
|
protected abstract void performApplySpans(SpannableStringBuilder builder, int begin, int end);
|
||||||
}
|
}
|
||||||
|
@ -79,9 +79,8 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||||||
@Override
|
@Override
|
||||||
public int reactTagForTouch(float touchX, float touchY) {
|
public int reactTagForTouch(float touchX, float touchY) {
|
||||||
for (NodeRegion nodeRegion : mNodeRegions) {
|
for (NodeRegion nodeRegion : mNodeRegions) {
|
||||||
if (nodeRegion.mLeft <= touchX && touchX < nodeRegion.mRight &&
|
if (nodeRegion.withinBounds(touchX, touchY)) {
|
||||||
nodeRegion.mTop <= touchY && touchY < nodeRegion.mBottom) {
|
return nodeRegion.getReactTag(touchX, touchY);
|
||||||
return nodeRegion.mTag;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
package com.facebook.react.flat;
|
package com.facebook.react.flat;
|
||||||
|
|
||||||
/* package */ final class NodeRegion {
|
/* package */ class NodeRegion {
|
||||||
/* package */ static final NodeRegion[] EMPTY_ARRAY = new NodeRegion[0];
|
/* package */ static final NodeRegion[] EMPTY_ARRAY = new NodeRegion[0];
|
||||||
/* package */ static final NodeRegion EMPTY = new NodeRegion(0, 0, 0, 0, -1);
|
/* package */ static final NodeRegion EMPTY = new NodeRegion(0, 0, 0, 0, -1);
|
||||||
|
|
||||||
@ -26,4 +26,12 @@ package com.facebook.react.flat;
|
|||||||
mBottom = bottom;
|
mBottom = bottom;
|
||||||
mTag = tag;
|
mTag = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ final boolean withinBounds(float touchX, float touchY) {
|
||||||
|
return mLeft <= touchX && touchX < mRight && mTop <= touchY && touchY < mBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ int getReactTag(float touchX, float touchY) {
|
||||||
|
return mTag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ package com.facebook.react.flat;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.text.Spannable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
|
||||||
import com.facebook.react.uimanager.ReactProp;
|
import com.facebook.react.uimanager.ReactProp;
|
||||||
@ -23,17 +24,19 @@ import com.facebook.react.uimanager.ReactProp;
|
|||||||
private @Nullable String mText;
|
private @Nullable String mText;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void collectText(SpannableStringBuilder builder) {
|
protected void performCollectText(SpannableStringBuilder builder) {
|
||||||
if (mText != null) {
|
if (mText != null) {
|
||||||
builder.append(mText);
|
builder.append(mText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RCTRawText cannot have any children, so no recursive calls needed.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applySpans(SpannableStringBuilder builder) {
|
protected void performApplySpans(SpannableStringBuilder builder, int begin, int end) {
|
||||||
// no spans and no children so nothing to do here.
|
builder.setSpan(
|
||||||
|
this,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "text")
|
@ReactProp(name = "text")
|
||||||
|
@ -174,6 +174,28 @@ import com.facebook.react.uimanager.ViewProps;
|
|||||||
notifyChanged(true);
|
notifyChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/* package */ void updateNodeRegion(float left, float top, float right, float bottom) {
|
||||||
|
if (mDrawCommand == null) {
|
||||||
|
super.updateNodeRegion(left, top, right, bottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeRegion nodeRegion = getNodeRegion();
|
||||||
|
Layout layout = null;
|
||||||
|
|
||||||
|
if (nodeRegion instanceof TextNodeRegion) {
|
||||||
|
layout = ((TextNodeRegion) nodeRegion).getLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout newLayout = mDrawCommand.getLayout();
|
||||||
|
if (nodeRegion.mLeft != left || nodeRegion.mTop != top ||
|
||||||
|
nodeRegion.mRight != right || nodeRegion.mBottom != bottom ||
|
||||||
|
layout != newLayout) {
|
||||||
|
setNodeRegion(new TextNodeRegion(left, top, right, bottom, getReactTag(), newLayout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getDefaultFontSize() {
|
protected int getDefaultFontSize() {
|
||||||
// top-level <Text /> should always specify font size.
|
// top-level <Text /> should always specify font size.
|
||||||
|
@ -31,43 +31,29 @@ import com.facebook.react.uimanager.ViewProps;
|
|||||||
|
|
||||||
private FontStylingSpan mFontStylingSpan = new FontStylingSpan();
|
private FontStylingSpan mFontStylingSpan = new FontStylingSpan();
|
||||||
|
|
||||||
// these 2 are only used between collectText() and applySpans() calls.
|
|
||||||
private int mTextBegin;
|
|
||||||
private int mTextEnd;
|
|
||||||
|
|
||||||
RCTVirtualText() {
|
RCTVirtualText() {
|
||||||
mFontStylingSpan.setFontSize(getDefaultFontSize());
|
mFontStylingSpan.setFontSize(getDefaultFontSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void collectText(SpannableStringBuilder builder) {
|
protected void performCollectText(SpannableStringBuilder builder) {
|
||||||
int childCount = getChildCount();
|
for (int i = 0, childCount = getChildCount(); i < childCount; ++i) {
|
||||||
|
|
||||||
mTextBegin = builder.length();
|
|
||||||
for (int i = 0; i < childCount; ++i) {
|
|
||||||
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
||||||
child.collectText(builder);
|
child.collectText(builder);
|
||||||
}
|
}
|
||||||
mTextEnd = builder.length();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applySpans(SpannableStringBuilder builder) {
|
protected void performApplySpans(SpannableStringBuilder builder, int begin, int end) {
|
||||||
if (mTextBegin == mTextEnd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mFontStylingSpan.freeze();
|
mFontStylingSpan.freeze();
|
||||||
|
|
||||||
builder.setSpan(
|
builder.setSpan(
|
||||||
mFontStylingSpan,
|
mFontStylingSpan,
|
||||||
mTextBegin,
|
begin,
|
||||||
mTextEnd,
|
end,
|
||||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
int childCount = getChildCount();
|
for (int i = 0, childCount = getChildCount(); i < childCount; ++i) {
|
||||||
|
|
||||||
for (int i = 0; i < childCount; ++i) {
|
|
||||||
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i);
|
||||||
child.applySpans(builder);
|
child.applySpans(builder);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap;
|
|||||||
float top = node.getLayoutY();
|
float top = node.getLayoutY();
|
||||||
float right = left + width;
|
float right = left + width;
|
||||||
float bottom = top + height;
|
float bottom = top + height;
|
||||||
updateNodeRegion(node, tag, left, top, right, bottom);
|
node.updateNodeRegion(left, top, right, bottom);
|
||||||
|
|
||||||
mViewsToUpdateBounds.add(node);
|
mViewsToUpdateBounds.add(node);
|
||||||
|
|
||||||
@ -170,6 +170,13 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap;
|
|||||||
|
|
||||||
isAndroidView = true;
|
isAndroidView = true;
|
||||||
needsCustomLayoutForChildren = androidView.needsCustomLayoutForChildren();
|
needsCustomLayoutForChildren = androidView.needsCustomLayoutForChildren();
|
||||||
|
} else if (node.isVirtualAnchor()) {
|
||||||
|
// If RCTText is mounted to View, virtual children will not receive any touch events
|
||||||
|
// because they don't get added to nodeRegions, so nodeRegions will be empty and
|
||||||
|
// FlatViewGroup.reactTagForTouch() will always return RCTText's id. To fix the issue,
|
||||||
|
// manually add nodeRegion so it will have exactly one NodeRegion, and virtual nodes will
|
||||||
|
// be able to receive touch events.
|
||||||
|
addNodeRegion(node.getNodeRegion());
|
||||||
}
|
}
|
||||||
|
|
||||||
collectStateRecursively(node, 0, 0, width, height, isAndroidView, needsCustomLayoutForChildren);
|
collectStateRecursively(node, 0, 0, width, height, isAndroidView, needsCustomLayoutForChildren);
|
||||||
@ -320,7 +327,7 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap;
|
|||||||
float right = left + width;
|
float right = left + width;
|
||||||
float bottom = top + height;
|
float bottom = top + height;
|
||||||
|
|
||||||
updateNodeRegion(node, tag, left, top, right, bottom);
|
node.updateNodeRegion(left, top, right, bottom);
|
||||||
|
|
||||||
if (node.mountsToView()) {
|
if (node.mountsToView()) {
|
||||||
ensureBackingViewIsCreated(node, tag, null);
|
ensureBackingViewIsCreated(node, tag, null);
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.flat;
|
||||||
|
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
/* package */ final class TextNodeRegion extends NodeRegion {
|
||||||
|
private final Layout mLayout;
|
||||||
|
|
||||||
|
TextNodeRegion(float left, float top, float right, float bottom, int tag, Layout layout) {
|
||||||
|
super(left, top, right, bottom, tag);
|
||||||
|
mLayout = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ Layout getLayout() {
|
||||||
|
return mLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ int getReactTag(float touchX, float touchY) {
|
||||||
|
int y = Math.round(touchY - mTop);
|
||||||
|
if (y >= mLayout.getLineTop(0) && y < mLayout.getLineBottom(mLayout.getLineCount() - 1)) {
|
||||||
|
float x = Math.round(touchX - mLeft);
|
||||||
|
int line = mLayout.getLineForVertical(y);
|
||||||
|
|
||||||
|
if (mLayout.getLineLeft(line) <= x && x <= mLayout.getLineRight(line)) {
|
||||||
|
int off = mLayout.getOffsetForHorizontal(line, x);
|
||||||
|
|
||||||
|
Spanned text = (Spanned) mLayout.getText();
|
||||||
|
RCTRawText[] link = text.getSpans(off, off, RCTRawText.class);
|
||||||
|
|
||||||
|
if (link.length != 0) {
|
||||||
|
return link[0].getReactTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getReactTag(touchX, touchY);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user