diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawTextLayout.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawTextLayout.java index 63d7037c8..764a15eef 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawTextLayout.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawTextLayout.java @@ -30,6 +30,10 @@ import android.text.Layout; mLayout = layout; } + public Layout getLayout() { + return mLayout; + } + @Override public void draw(FlatViewGroup parent, Canvas canvas) { float left = getLeft(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java index 1134e196d..83bf025bb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -171,14 +171,21 @@ import com.facebook.react.uimanager.ViewProps; mNodeRegions = nodeRegion; } - /* package */ final NodeRegion getNodeRegion() { - return mNodeRegion; + /* package */ void updateNodeRegion(float left, float top, float right, float bottom) { + 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; } + /* package */ final NodeRegion getNodeRegion() { + return mNodeRegion; + } + /** * Sets boundaries of the View that this node maps to relative to the parent left/top coordinate. */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java index bf0bfa795..0effe2b84 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatTextShadowNode.java @@ -18,17 +18,9 @@ import com.facebook.react.uimanager.ReactShadowNode; */ /* package */ abstract class FlatTextShadowNode extends FlatShadowNode { - /** - * Recursively visits FlatTextShadowNode and its children, - * appending text to SpannableStringBuilder. - */ - protected abstract void collectText(SpannableStringBuilder builder); - - /** - * Recursively visits FlatTextShadowNode and its children, - * applying spans to SpannableStringBuilder. - */ - protected abstract void applySpans(SpannableStringBuilder builder); + // these 2 are only used between collectText() and applySpans() calls. + private int mTextBegin; + private int mTextEnd; /** * Propagates changes up to RCTText without dirtying current node. @@ -44,4 +36,27 @@ import com.facebook.react.uimanager.ReactShadowNode; public boolean isVirtual() { 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); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java index b4157b54a..d8a915539 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -79,9 +79,8 @@ import com.facebook.react.uimanager.ReactCompoundView; @Override public int reactTagForTouch(float touchX, float touchY) { for (NodeRegion nodeRegion : mNodeRegions) { - if (nodeRegion.mLeft <= touchX && touchX < nodeRegion.mRight && - nodeRegion.mTop <= touchY && touchY < nodeRegion.mBottom) { - return nodeRegion.mTag; + if (nodeRegion.withinBounds(touchX, touchY)) { + return nodeRegion.getReactTag(touchX, touchY); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/NodeRegion.java b/ReactAndroid/src/main/java/com/facebook/react/flat/NodeRegion.java index 6ea1a1abc..db4f8109b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/NodeRegion.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/NodeRegion.java @@ -9,7 +9,7 @@ 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 = new NodeRegion(0, 0, 0, 0, -1); @@ -26,4 +26,12 @@ package com.facebook.react.flat; mBottom = bottom; 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; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java index 18bd28ced..880cc65ec 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTRawText.java @@ -11,6 +11,7 @@ package com.facebook.react.flat; import javax.annotation.Nullable; +import android.text.Spannable; import android.text.SpannableStringBuilder; import com.facebook.react.uimanager.ReactProp; @@ -23,17 +24,19 @@ import com.facebook.react.uimanager.ReactProp; private @Nullable String mText; @Override - protected void collectText(SpannableStringBuilder builder) { + protected void performCollectText(SpannableStringBuilder builder) { if (mText != null) { builder.append(mText); } - - // RCTRawText cannot have any children, so no recursive calls needed. } @Override - protected void applySpans(SpannableStringBuilder builder) { - // no spans and no children so nothing to do here. + protected void performApplySpans(SpannableStringBuilder builder, int begin, int end) { + builder.setSpan( + this, + begin, + end, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } @ReactProp(name = "text") diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java index 5bc51a93d..1b0a59b2f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTText.java @@ -174,6 +174,28 @@ import com.facebook.react.uimanager.ViewProps; 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 protected int getDefaultFontSize() { // top-level should always specify font size. diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java index ec1a1ef1b..b38e9e9dd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTVirtualText.java @@ -31,43 +31,29 @@ import com.facebook.react.uimanager.ViewProps; private FontStylingSpan mFontStylingSpan = new FontStylingSpan(); - // these 2 are only used between collectText() and applySpans() calls. - private int mTextBegin; - private int mTextEnd; - RCTVirtualText() { mFontStylingSpan.setFontSize(getDefaultFontSize()); } @Override - protected void collectText(SpannableStringBuilder builder) { - int childCount = getChildCount(); - - mTextBegin = builder.length(); - for (int i = 0; i < childCount; ++i) { + protected void performCollectText(SpannableStringBuilder builder) { + for (int i = 0, childCount = getChildCount(); i < childCount; ++i) { FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i); child.collectText(builder); } - mTextEnd = builder.length(); } @Override - protected void applySpans(SpannableStringBuilder builder) { - if (mTextBegin == mTextEnd) { - return; - } - + protected void performApplySpans(SpannableStringBuilder builder, int begin, int end) { mFontStylingSpan.freeze(); builder.setSpan( mFontStylingSpan, - mTextBegin, - mTextEnd, + begin, + end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - int childCount = getChildCount(); - - for (int i = 0; i < childCount; ++i) { + for (int i = 0, childCount = getChildCount(); i < childCount; ++i) { FlatTextShadowNode child = (FlatTextShadowNode) getChildAt(i); child.applySpans(builder); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java index 3875f01de..be39ff76e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -63,7 +63,7 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap; float top = node.getLayoutY(); float right = left + width; float bottom = top + height; - updateNodeRegion(node, tag, left, top, right, bottom); + node.updateNodeRegion(left, top, right, bottom); mViewsToUpdateBounds.add(node); @@ -170,6 +170,13 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap; isAndroidView = true; 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); @@ -320,7 +327,7 @@ import com.facebook.react.uimanager.CatalystStylesDiffMap; float right = left + width; float bottom = top + height; - updateNodeRegion(node, tag, left, top, right, bottom); + node.updateNodeRegion(left, top, right, bottom); if (node.mountsToView()) { ensureBackingViewIsCreated(node, tag, null); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/TextNodeRegion.java b/ReactAndroid/src/main/java/com/facebook/react/flat/TextNodeRegion.java new file mode 100644 index 000000000..e6d489458 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/TextNodeRegion.java @@ -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); + } +}