diff --git a/Examples/UIExplorer/js/TextExample.android.js b/Examples/UIExplorer/js/TextExample.android.js index 53715fa84948f0..fe4dcac317a881 100644 --- a/Examples/UIExplorer/js/TextExample.android.js +++ b/Examples/UIExplorer/js/TextExample.android.js @@ -390,9 +390,10 @@ var TextExample = React.createClass({ This text is selectable if you click-and-hold, and will offer the native Android selection menus. - + - This text contains an inline image . Neat, huh? + This text contains an inline blue view and + an inline image . Neat, huh? diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java index 6bb7cf663a59a5..dae23c8e1717cd 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java @@ -7,6 +7,11 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +// NOTE: Changes in this file must be imported from this css-layout PR: https://github.com/facebook/css-layout/pull/202 + package com.facebook.csslayout; import javax.annotation.Nullable; @@ -24,7 +29,7 @@ /** * A CSS Node. It has a style object you can manipulate at {@link #style}. After calling - * {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout. + * {@link #calculateLayout(CSSLayoutContext)}, {@link #layout} will be filled with the results of the layout. */ public class CSSNode { @@ -72,6 +77,11 @@ public static interface MeasureFunction { private LayoutState mLayoutState = LayoutState.DIRTY; private boolean mIsTextNode = false; + public static final Iterable NO_CSS_NODES = new ArrayList(0); + public Iterable getChildrenIterable() { + return mChildren == null ? NO_CSS_NODES : mChildren; + } + public int getChildCount() { return mChildren == null ? 0 : mChildren.size(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/IViewManagerWithChildren.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IViewManagerWithChildren.java new file mode 100644 index 00000000000000..8b69e3c379e2f3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IViewManagerWithChildren.java @@ -0,0 +1,24 @@ +/** + * 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.uimanager; + +public interface IViewManagerWithChildren { + /** + * Returns whether this View type needs to handle laying out its own children instead of + * deferring to the standard css-layout algorithm. + * Returns true for the layout to *not* be automatically invoked. Instead onLayout will be + * invoked as normal and it is the View instance's responsibility to properly call layout on its + * children. + * Returns false for the default behavior of automatically laying out children without going + * through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not* + * call layout on its children. + */ + public boolean needsCustomLayoutForChildren(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeKind.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeKind.java new file mode 100644 index 00000000000000..8dc3309189700d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeKind.java @@ -0,0 +1,23 @@ +/** + * 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.uimanager; + +public enum NativeKind { + // Node is in the native hierarchy and the HierarchyOptimizer should assume it can host children + // (e.g. because it's a ViewGroup). Note that it's okay if the node doesn't support children. When + // the HierarchyOptimizer generates children manipulation commands for that node, the + // HierarchyManager will catch this case and throw an exception. + PARENT, + // Node is in the native hierarchy, it may have children, but it cannot host them itself (e.g. + // because it isn't a ViewGroup). Consequently, its children need to be hosted by an ancestor. + LEAF, + // Node is not in the native hierarchy. + NONE +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index b6c66d30ffdba5..dca6af00cb345b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -164,16 +164,16 @@ public void updateLayout( // Check if the parent of the view has to layout the view, or the child has to lay itself out. if (!mRootTags.get(parentTag)) { ViewManager parentViewManager = mTagsToViewManagers.get(parentTag); - ViewGroupManager parentViewGroupManager; - if (parentViewManager instanceof ViewGroupManager) { - parentViewGroupManager = (ViewGroupManager) parentViewManager; + IViewManagerWithChildren parentViewManagerWithChildren; + if (parentViewManager instanceof IViewManagerWithChildren) { + parentViewManagerWithChildren = (IViewManagerWithChildren) parentViewManager; } else { throw new IllegalViewOperationException( - "Trying to use view with tag " + tag + - " as a parent, but its Manager doesn't extends ViewGroupManager"); + "Trying to use view with tag " + parentTag + + " as a parent, but its Manager doesn't implement IViewManagerWithChildren"); } - if (parentViewGroupManager != null - && !parentViewGroupManager.needsCustomLayoutForChildren()) { + if (parentViewManagerWithChildren != null + && !parentViewManagerWithChildren.needsCustomLayoutForChildren()) { updateLayout(viewToUpdate, x, y, width, height); } } else { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java index 2d921bb0f1c230..d62ba3e15aeae3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java @@ -68,6 +68,15 @@ private static class NodeIndexPair { private final ShadowNodeRegistry mShadowNodeRegistry; private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray(); + public static void assertNodeSupportedWithoutOptimizer(ReactShadowNode node) { + // NativeKind.LEAF nodes require the optimizer. They are not ViewGroups so they cannot host + // their native children themselves. Their native children need to be hoisted by the optimizer + // to an ancestor which is a ViewGroup. + Assertions.assertCondition( + node.getNativeKind() != NativeKind.LEAF, + "Nodes with NativeKind.LEAF are not supported when the optimizer is disabled"); + } + public NativeViewHierarchyOptimizer( UIViewOperationQueue uiViewOperationQueue, ShadowNodeRegistry shadowNodeRegistry) { @@ -83,6 +92,7 @@ public void handleCreateView( ThemedReactContext themedContext, @Nullable ReactStylesDiffMap initialProps) { if (!ENABLED) { + assertNodeSupportedWithoutOptimizer(node); int tag = node.getReactTag(); mUIViewOperationQueue.enqueueCreateView( themedContext, @@ -96,7 +106,7 @@ public void handleCreateView( isLayoutOnlyAndCollapsable(initialProps); node.setIsLayoutOnly(isLayoutOnly); - if (!isLayoutOnly) { + if (node.getNativeKind() != NativeKind.NONE) { mUIViewOperationQueue.enqueueCreateView( themedContext, node.getReactTag(), @@ -122,6 +132,7 @@ public void handleUpdateView( String className, ReactStylesDiffMap props) { if (!ENABLED) { + assertNodeSupportedWithoutOptimizer(node); mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props); return; } @@ -151,6 +162,7 @@ public void handleManageChildren( ViewAtIndex[] viewsToAdd, int[] tagsToDelete) { if (!ENABLED) { + assertNodeSupportedWithoutOptimizer(nodeToManage); mUIViewOperationQueue.enqueueManageChildren( nodeToManage.getReactTag(), indicesToRemove, @@ -191,6 +203,7 @@ public void handleSetChildren( ReadableArray childrenTags ) { if (!ENABLED) { + assertNodeSupportedWithoutOptimizer(nodeToManage); mUIViewOperationQueue.enqueueSetChildren( nodeToManage.getReactTag(), childrenTags); @@ -210,8 +223,9 @@ public void handleSetChildren( */ public void handleUpdateLayout(ReactShadowNode node) { if (!ENABLED) { + assertNodeSupportedWithoutOptimizer(node); mUIViewOperationQueue.enqueueUpdateLayout( - Assertions.assertNotNull(node.getParent()).getReactTag(), + Assertions.assertNotNull(node.getLayoutParent()).getReactTag(), node.getReactTag(), node.getScreenX(), node.getScreenY(), @@ -223,6 +237,12 @@ public void handleUpdateLayout(ReactShadowNode node) { applyLayoutBase(node); } + public void handleForceViewToBeNonLayoutOnly(ReactShadowNode node) { + if (node.isLayoutOnly()) { + transitionLayoutOnlyViewToNativeView(node, null); + } + } + /** * Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native * hierarchy. Should be called after all updateLayout calls for a batch have been handled. @@ -231,16 +251,18 @@ public void onBatchComplete() { mTagsWithLayoutVisited.clear(); } - private NodeIndexPair walkUpUntilNonLayoutOnly( + private NodeIndexPair walkUpUntilNativeKindIsParent( ReactShadowNode node, int indexInNativeChildren) { - while (node.isLayoutOnly()) { + while (node.getNativeKind() != NativeKind.PARENT) { ReactShadowNode parent = node.getParent(); if (parent == null) { return null; } - indexInNativeChildren = indexInNativeChildren + parent.getNativeOffsetForChild(node); + indexInNativeChildren = indexInNativeChildren + + (node.getNativeKind() == NativeKind.LEAF ? 1 : 0) + + parent.getNativeOffsetForChild(node); node = parent; } @@ -249,8 +271,8 @@ private NodeIndexPair walkUpUntilNonLayoutOnly( private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) { int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index)); - if (parent.isLayoutOnly()) { - NodeIndexPair result = walkUpUntilNonLayoutOnly(parent, indexInNativeChildren); + if (parent.getNativeKind() != NativeKind.PARENT) { + NodeIndexPair result = walkUpUntilNativeKindIsParent(parent, indexInNativeChildren); if (result == null) { // If the parent hasn't been attached to its native parent yet, don't issue commands to the // native hierarchy. We'll do that when the parent node actually gets attached somewhere. @@ -260,20 +282,26 @@ private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int in indexInNativeChildren = result.index; } - if (!child.isLayoutOnly()) { - addNonLayoutNode(parent, child, indexInNativeChildren); + if (child.getNativeKind() != NativeKind.NONE) { + addNativeChild(parent, child, indexInNativeChildren); } else { - addLayoutOnlyNode(parent, child, indexInNativeChildren); + addNonNativeChild(parent, child, indexInNativeChildren); } } /** - * For handling node removal from manageChildren. In the case of removing a layout-only node, we - * need to instead recursively remove all its children from their native parents. + * For handling node removal from manageChildren. In the case of removing a node which isn't + * hosting its own children (e.g. layout-only or NativeKind.LEAF), we need to recursively remove + * all its children from their native parents. */ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) { - ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent(); + if (nodeToRemove.getNativeKind() != NativeKind.PARENT) { + for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { + removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete); + } + } + ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent(); if (nativeNodeToRemoveFrom != null) { int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove); nativeNodeToRemoveFrom.removeNativeChildAt(index); @@ -283,21 +311,17 @@ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDe new int[]{index}, null, shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null); - } else { - for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { - removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete); - } } } - private void addLayoutOnlyNode( - ReactShadowNode nonLayoutOnlyNode, - ReactShadowNode layoutOnlyNode, + private void addNonNativeChild( + ReactShadowNode nativeParent, + ReactShadowNode nonNativeChild, int index) { - addGrandchildren(nonLayoutOnlyNode, layoutOnlyNode, index); + addGrandchildren(nativeParent, nonNativeChild, index); } - private void addNonLayoutNode( + private void addNativeChild( ReactShadowNode parent, ReactShadowNode child, int index) { @@ -307,13 +331,17 @@ private void addNonLayoutNode( null, new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)}, null); + + if (child.getNativeKind() != NativeKind.PARENT) { + addGrandchildren(parent, child, index + 1); + } } private void addGrandchildren( ReactShadowNode nativeParent, ReactShadowNode child, int index) { - Assertions.assertCondition(!nativeParent.isLayoutOnly()); + Assertions.assertCondition(child.getNativeKind() != NativeKind.PARENT); // `child` can't hold native children. Add all of `child`'s children to `parent`. int currentIndex = index; @@ -321,16 +349,15 @@ private void addGrandchildren( ReactShadowNode grandchild = child.getChildAt(i); Assertions.assertCondition(grandchild.getNativeParent() == null); - if (grandchild.isLayoutOnly()) { - // Adding this child could result in adding multiple native views - int grandchildCountBefore = nativeParent.getNativeChildCount(); - addLayoutOnlyNode(nativeParent, grandchild, currentIndex); - int grandchildCountAfter = nativeParent.getNativeChildCount(); - currentIndex += grandchildCountAfter - grandchildCountBefore; + // Adding this child could result in adding multiple native views + int grandchildCountBefore = nativeParent.getNativeChildCount(); + if (grandchild.getNativeKind() == NativeKind.NONE) { + addNonNativeChild(nativeParent, grandchild, currentIndex); } else { - addNonLayoutNode(nativeParent, grandchild, currentIndex); - currentIndex++; + addNativeChild(nativeParent, grandchild, currentIndex); } + int grandchildCountAfter = nativeParent.getNativeChildCount(); + currentIndex += grandchildCountAfter - grandchildCountBefore; } } @@ -349,7 +376,7 @@ private void applyLayoutBase(ReactShadowNode node) { int x = node.getScreenX(); int y = node.getScreenY(); - while (parent != null && parent.isLayoutOnly()) { + while (parent != null && parent.getNativeKind() != NativeKind.PARENT) { // TODO(7854667): handle and test proper clipping x += Math.round(parent.getLayoutX()); y += Math.round(parent.getLayoutY()); @@ -361,10 +388,10 @@ private void applyLayoutBase(ReactShadowNode node) { } private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) { - if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) { + if (toUpdate.getNativeKind() != NativeKind.NONE && toUpdate.getNativeParent() != null) { int tag = toUpdate.getReactTag(); mUIViewOperationQueue.enqueueUpdateLayout( - toUpdate.getNativeParent().getReactTag(), + toUpdate.getLayoutParent().getReactTag(), tag, x, y, diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java index df8ad675d294e1..e62ddd5ad9670e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -13,6 +13,7 @@ import java.util.ArrayList; +import com.facebook.csslayout.CSSLayoutContext; import com.facebook.csslayout.CSSNode; import com.facebook.infer.annotation.Assertions; import com.facebook.react.uimanager.annotations.ReactPropertyHolder; @@ -49,6 +50,7 @@ public class ReactShadowNode extends CSSNode { private @Nullable ThemedReactContext mThemedContext; private boolean mShouldNotifyOnLayout; private boolean mNodeUpdated = true; + private @Nullable ReactShadowNode mLayoutParent; // layout-only nodes private boolean mIsLayoutOnly; @@ -70,15 +72,24 @@ public boolean isVirtual() { /** * Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It - * means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren} - * operation on such views. Good example is {@code InputText} view that may have children - * {@code Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} - * view. + * means that all of its descendants will be "virtual" nodes. Good example is {@code InputText} + * view that may have children {@code Text} nodes but this whole hierarchy will be mapped to a + * single android {@link EditText} view. */ public boolean isVirtualAnchor() { return false; } + /** + * When constructing the native tree, nodes that return {@code true} will be treated as leaves. + * Instead of adding this view's native children as subviews of it, they will be added as subviews + * of an ancestor. In other words, this view wants to support native children but it cannot host + * them itself (e.g. it isn't a ViewGroup). + */ + public boolean hoistNativeChildren() { + return false; + } + public final String getViewClass() { return Assertions.assertNotNull(mViewClassName); } @@ -113,6 +124,10 @@ public boolean hasUnseenUpdates() { protected void dirty() { if (!isVirtual()) { super.dirty(); + } else if (getParent() != null) { + // Virtual nodes aren't involved in layout but they need to have the dirty signal + // propagated to their ancestors. + getParent().dirty(); } } @@ -122,7 +137,7 @@ public void addChildAt(CSSNode child, int i) { markUpdated(); ReactShadowNode node = (ReactShadowNode) child; - int increase = node.mIsLayoutOnly ? node.mTotalNativeChildren : 1; + int increase = node.getTotalNativeNodeContributionToParent(); mTotalNativeChildren += increase; updateNativeChildrenCountInParent(increase); @@ -133,7 +148,7 @@ public ReactShadowNode removeChildAt(int i) { ReactShadowNode removed = (ReactShadowNode) super.removeChildAt(i); markUpdated(); - int decrease = removed.mIsLayoutOnly ? removed.mTotalNativeChildren : 1; + int decrease = removed.getTotalNativeNodeContributionToParent(); mTotalNativeChildren -= decrease; updateNativeChildrenCountInParent(-decrease); return removed; @@ -143,7 +158,7 @@ public void removeAllChildren() { int decrease = 0; for (int i = getChildCount() - 1; i >= 0; i--) { ReactShadowNode removed = (ReactShadowNode) super.removeChildAt(i); - decrease += removed.mIsLayoutOnly ? removed.mTotalNativeChildren : 1; + decrease += removed.getTotalNativeNodeContributionToParent(); } markUpdated(); @@ -152,11 +167,11 @@ public void removeAllChildren() { } private void updateNativeChildrenCountInParent(int delta) { - if (mIsLayoutOnly) { + if (getNativeKind() != NativeKind.PARENT) { ReactShadowNode parent = getParent(); while (parent != null) { parent.mTotalNativeChildren += delta; - if (!parent.mIsLayoutOnly) { + if (parent.getNativeKind() == NativeKind.PARENT) { break; } parent = parent.getParent(); @@ -169,7 +184,7 @@ private void updateNativeChildrenCountInParent(int delta) { * layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()} * or require layouting (marked with {@link #dirty()}). */ - public void onBeforeLayout() { + public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { } public final void updateProperties(ReactStylesDiffMap props) { @@ -240,6 +255,15 @@ public final ReactShadowNode getChildAt(int i) { return (ReactShadowNode) super.getParent(); } + // Returns the node that is responsible for laying out this node. + public final @Nullable ReactShadowNode getLayoutParent() { + return mLayoutParent != null ? mLayoutParent : getNativeParent(); + } + + public final void setLayoutParent(@Nullable ReactShadowNode layoutParent) { + mLayoutParent = layoutParent; + } + /** * Get the {@link ThemedReactContext} associated with this {@link ReactShadowNode}. This will * never change during the lifetime of a {@link ReactShadowNode} instance, but different instances @@ -266,8 +290,8 @@ public boolean shouldNotifyOnLayout() { * corresponding to this node. */ public void addNativeChildAt(ReactShadowNode child, int nativeIndex) { - Assertions.assertCondition(!mIsLayoutOnly); - Assertions.assertCondition(!child.mIsLayoutOnly); + Assertions.assertCondition(getNativeKind() == NativeKind.PARENT); + Assertions.assertCondition(child.getNativeKind() != NativeKind.NONE); if (mNativeChildren == null) { mNativeChildren = new ArrayList<>(4); @@ -321,10 +345,25 @@ public boolean isLayoutOnly() { return mIsLayoutOnly; } + public NativeKind getNativeKind() { + return + isVirtual() || isLayoutOnly() ? NativeKind.NONE : + hoistNativeChildren() ? NativeKind.LEAF : + NativeKind.PARENT; + } + public int getTotalNativeChildren() { return mTotalNativeChildren; } + private int getTotalNativeNodeContributionToParent() { + NativeKind kind = getNativeKind(); + return + kind == NativeKind.NONE ? mTotalNativeChildren : + kind == NativeKind.LEAF ? 1 + mTotalNativeChildren : + 1; // kind == NativeKind.PARENT + } + /** * Returns the offset within the native children owned by all layout-only nodes in the subtree * rooted at this node for the given child. Put another way, this returns the number of native @@ -363,7 +402,7 @@ public int getNativeOffsetForChild(ReactShadowNode child) { found = true; break; } - index += (current.mIsLayoutOnly ? current.getTotalNativeChildren() : 1); + index += current.getTotalNativeNodeContributionToParent(); } if (!found) { throw new RuntimeException("Child " + child.mReactTag + " was not a child of " + mReactTag); @@ -398,4 +437,13 @@ public int getScreenWidth() { public int getScreenHeight() { return Math.round(mAbsoluteBottom - mAbsoluteTop); } + + public static final Iterable NO_CSS_NODES = new ArrayList(0); + public Iterable calculateLayoutOnChildren(CSSLayoutContext layoutContext, UIViewOperationQueue viewOperationsQueue) { + return isVirtualAnchor() ? + // All of the descendants are virtual so none of them are involved in layout. + NO_CSS_NODES : + // Just return the children. Flexbox calculations have already been run on them. + getChildrenIterable(); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index b1d52cf0f38525..7e9e8ce689fd15 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -13,7 +13,9 @@ import java.util.Arrays; import java.util.List; +import com.facebook.csslayout.CSSConstants; import com.facebook.csslayout.CSSLayoutContext; +import com.facebook.csslayout.CSSNode; import com.facebook.infer.annotation.Assertions; import com.facebook.react.animation.Animation; import com.facebook.react.bridge.Arguments; @@ -310,14 +312,12 @@ public void manageChildren( cssNodeToManage.addChildAt(cssNodeToAdd, viewAtIndex.mIndex); } - if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) { - mNativeViewHierarchyOptimizer.handleManageChildren( - cssNodeToManage, - indicesToRemove, - tagsToRemove, - viewsToAdd, - tagsToDelete); - } + mNativeViewHierarchyOptimizer.handleManageChildren( + cssNodeToManage, + indicesToRemove, + tagsToRemove, + viewsToAdd, + tagsToDelete); for (int i = 0; i < tagsToDelete.length; i++) { removeShadowNode(mShadowNodeRegistry.getNode(tagsToDelete[i])); @@ -346,11 +346,9 @@ public void setChildren( cssNodeToManage.addChildAt(cssNodeToAdd, i); } - if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) { - mNativeViewHierarchyOptimizer.handleSetChildren( - cssNodeToManage, - childrenTags); - } + mNativeViewHierarchyOptimizer.handleSetChildren( + cssNodeToManage, + childrenTags); } /** @@ -569,7 +567,7 @@ public void configureNextLayoutAnimation( public void setJSResponder(int reactTag, boolean blockNativeResponder) { assertViewExists(reactTag, "setJSResponder"); ReactShadowNode node = mShadowNodeRegistry.getNode(reactTag); - while (node.isVirtual() || node.isLayoutOnly()) { + while (node.getNativeKind() == NativeKind.NONE) { node = node.getParent(); } mOperationsQueue.enqueueSetJSResponder(node.getReactTag(), reactTag, blockNativeResponder); @@ -699,14 +697,14 @@ private void assertViewExists(int reactTag, String operationNameForExceptionMess private void assertNodeDoesNotNeedCustomLayoutForChildren(ReactShadowNode node) { ViewManager viewManager = Assertions.assertNotNull(mViewManagers.get(node.getViewClass())); - ViewGroupManager viewGroupManager; - if (viewManager instanceof ViewGroupManager) { - viewGroupManager = (ViewGroupManager) viewManager; + IViewManagerWithChildren viewManagerWithChildren; + if (viewManager instanceof IViewManagerWithChildren) { + viewManagerWithChildren = (IViewManagerWithChildren) viewManager; } else { throw new IllegalViewOperationException("Trying to use view " + node.getViewClass() + " as a parent, but its Manager doesn't extends ViewGroupManager"); } - if (viewGroupManager != null && viewGroupManager.needsCustomLayoutForChildren()) { + if (viewManagerWithChildren != null && viewManagerWithChildren.needsCustomLayoutForChildren()) { throw new IllegalViewOperationException( "Trying to measure a view using measureLayout/measureLayoutRelativeToParent relative to" + " an ancestor that requires custom layout for it's children (" + node.getViewClass() + @@ -721,7 +719,7 @@ private void notifyOnBeforeLayoutRecursive(ReactShadowNode cssNode) { for (int i = 0; i < cssNode.getChildCount(); i++) { notifyOnBeforeLayoutRecursive(cssNode.getChildAt(i)); } - cssNode.onBeforeLayout(); + cssNode.onBeforeLayout(mNativeViewHierarchyOptimizer); } protected void calculateRootLayout(ReactShadowNode cssRoot) { @@ -744,14 +742,12 @@ protected void applyUpdatesRecursive( return; } - if (!cssNode.isVirtualAnchor()) { - for (int i = 0; i < cssNode.getChildCount(); i++) { - applyUpdatesRecursive( - cssNode.getChildAt(i), - absoluteX + cssNode.getLayoutX(), - absoluteY + cssNode.getLayoutY(), - eventDispatcher); - } + for (CSSNode cssChild : cssNode.calculateLayoutOnChildren(mLayoutContext, mOperationsQueue)) { + applyUpdatesRecursive( + (ReactShadowNode) cssChild, + absoluteX + cssNode.getLayoutX(), + absoluteY + cssNode.getLayoutY(), + eventDispatcher); } int tag = cssNode.getReactTag(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index b04af822a124cb..508f5703457909 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -9,6 +9,8 @@ package com.facebook.react.uimanager; +import android.view.View; + import javax.annotation.Nullable; import java.util.List; @@ -25,6 +27,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.systrace.Systrace; @@ -516,4 +519,9 @@ public void execute (NativeViewHierarchyManager nvhm) { public void addUIBlock (UIBlock block) { mUIImplementation.addUIBlock(block); } + + public View resolveView(int tag) { + UiThreadUtil.assertOnUiThread(); + return mUIImplementation.getUIViewOperationQueue().getNativeViewHierarchyManager().resolveView(tag); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java index a5094e7d5b8db4..e6c42b997dc9ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -22,7 +22,8 @@ * Class providing children management API for view managers of classes extending ViewGroup. */ public abstract class ViewGroupManager - extends BaseViewManager { + extends BaseViewManager + implements IViewManagerWithChildren { public static WeakHashMap mZIndexHash = new WeakHashMap<>(); @@ -134,6 +135,7 @@ public void removeAllViews(T parent) { * through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not* * call layout on its children. */ + @Override public boolean needsCustomLayoutForChildren() { return false; } 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 5178281a1d8c7a..6190e6cd1e6e8a 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 @@ -12,7 +12,9 @@ import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import android.graphics.Typeface; import android.text.BoringLayout; @@ -30,6 +32,7 @@ import android.widget.TextView; import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.CSSLayoutContext; import com.facebook.csslayout.CSSMeasureMode; import com.facebook.csslayout.CSSNode; import com.facebook.csslayout.MeasureOutput; @@ -38,6 +41,7 @@ import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.UIViewOperationQueue; @@ -60,7 +64,7 @@ */ public class ReactTextShadowNode extends LayoutShadowNode { - private static final String INLINE_IMAGE_PLACEHOLDER = "I"; + private static final String INLINE_VIEW_PLACEHOLDER = "I"; public static final int UNSET = -1; @VisibleForTesting @@ -102,29 +106,49 @@ public void execute(SpannableStringBuilder sb) { private static void buildSpannedFromTextCSSNode( ReactTextShadowNode textCSSNode, SpannableStringBuilder sb, - List ops) { + List ops, + boolean supportsInlineViews, + Map inlineViews) { int start = sb.length(); if (textCSSNode.mText != null) { sb.append(textCSSNode.mText); } for (int i = 0, length = textCSSNode.getChildCount(); i < length; i++) { - CSSNode child = textCSSNode.getChildAt(i); + ReactShadowNode child = textCSSNode.getChildAt(i); if (child instanceof ReactTextShadowNode) { - buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops); + buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops, supportsInlineViews, inlineViews); } else if (child instanceof ReactTextInlineImageShadowNode) { // We make the image take up 1 character in the span and put a corresponding character into // the text so that the image doesn't run over any following text. - sb.append(INLINE_IMAGE_PLACEHOLDER); + sb.append(INLINE_VIEW_PLACEHOLDER); ops.add( new SetSpanOperation( - sb.length() - INLINE_IMAGE_PLACEHOLDER.length(), + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), sb.length(), ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan())); + } else if (supportsInlineViews) { + int reactTag = child.getReactTag(); + float width = child.getStyleWidth(); + float height = child.getStyleHeight(); + + if (CSSConstants.isUndefined(width) || CSSConstants.isUndefined(height)) { + throw new IllegalViewOperationException("Views nested within a must have a width and height"); + } + + // We make the inline view take up 1 character in the span and put a corresponding character into + // the text so that the inline view doesn't run over any following text. + sb.append(INLINE_VIEW_PLACEHOLDER); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + inlineViews.put(reactTag, child); } else { - throw new IllegalViewOperationException("Unexpected view type nested under text node: " + throw new IllegalViewOperationException("Unexpected view type nested under a or node: " + child.getClass()); } - ((ReactShadowNode) child).markUpdateSeen(); + child.markUpdateSeen(); } int end = sb.length(); if (end >= start) { @@ -172,7 +196,12 @@ private static void buildSpannedFromTextCSSNode( } } - protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) { + // nativeViewHierarchyOptimizer can be null as long as the textCSSNode does not support inline views. + protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode, + NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { + Assertions.assertCondition( + !textCSSNode.supportsInlineViews() || nativeViewHierarchyOptimizer != null, + "nativeViewHierarchyOptimizer is required when the textCSSNode supports inline views"); SpannableStringBuilder sb = new SpannableStringBuilder(); // TODO(5837930): Investigate whether it's worth optimizing this part and do it if so @@ -180,7 +209,8 @@ protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) { // up-to-bottom, otherwise all the spannables that are withing the region for which one may set // a new spannable will be wiped out List ops = new ArrayList<>(); - buildSpannedFromTextCSSNode(textCSSNode, sb, ops); + Map inlineViews = textCSSNode.supportsInlineViews() ? new HashMap() : null; + buildSpannedFromTextCSSNode(textCSSNode, sb, ops, textCSSNode.supportsInlineViews(), inlineViews); if (textCSSNode.mFontSize == UNSET) { sb.setSpan( new AbsoluteSizeSpan((int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))), @@ -190,16 +220,34 @@ protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) { } textCSSNode.mContainsImages = false; - textCSSNode.mHeightOfTallestInlineImage = Float.NaN; + textCSSNode.mInlineViews = inlineViews; + textCSSNode.mHeightOfTallestInlineViewOrImage = Float.NaN; - // While setting the Spans on the final text, we also check whether any of them are images + // While setting the Spans on the final text, we also check whether any of them are inline views + // or images. for (int i = ops.size() - 1; i >= 0; i--) { SetSpanOperation op = ops.get(i); - if (op.what instanceof TextInlineImageSpan) { - int height = ((TextInlineImageSpan)op.what).getHeight(); - textCSSNode.mContainsImages = true; - if (Float.isNaN(textCSSNode.mHeightOfTallestInlineImage) || height > textCSSNode.mHeightOfTallestInlineImage) { - textCSSNode.mHeightOfTallestInlineImage = height; + boolean isInlineImage = op.what instanceof TextInlineImageSpan; + if (isInlineImage || op.what instanceof TextInlineViewPlaceholderSpan) { + int height; + if (isInlineImage) { + height = ((TextInlineImageSpan)op.what).getHeight(); + textCSSNode.mContainsImages = true; + } else { + TextInlineViewPlaceholderSpan placeholder = (TextInlineViewPlaceholderSpan) op.what; + height = placeholder.getHeight(); + + // Inline views cannot be layout-only because the ReactTextView needs to be able to grab + // ahold of them on the UI thread to size and position them. + ReactShadowNode childNode = inlineViews.get(placeholder.getReactTag()); + nativeViewHierarchyOptimizer.handleForceViewToBeNonLayoutOnly(childNode); + + // The ReactTextView is responsible for laying out the inline views. + childNode.setLayoutParent(textCSSNode); + } + + if (Float.isNaN(textCSSNode.mHeightOfTallestInlineViewOrImage) || height > textCSSNode.mHeightOfTallestInlineViewOrImage) { + textCSSNode.mHeightOfTallestInlineViewOrImage = height; } } op.execute(sb); @@ -346,7 +394,8 @@ private static int parseNumericFontWeight(String fontWeightString) { private final boolean mIsVirtual; protected boolean mContainsImages = false; - private float mHeightOfTallestInlineImage = Float.NaN; + private Map mInlineViews; + private float mHeightOfTallestInlineViewOrImage = Float.NaN; public ReactTextShadowNode(boolean isVirtual) { mIsVirtual = isVirtual; @@ -359,17 +408,17 @@ public ReactTextShadowNode(boolean isVirtual) { // and the height of the inline images. public float getEffectiveLineHeight() { boolean useInlineViewHeight = !Float.isNaN(mLineHeight) && - !Float.isNaN(mHeightOfTallestInlineImage) && - mHeightOfTallestInlineImage > mLineHeight; - return useInlineViewHeight ? mHeightOfTallestInlineImage : mLineHeight; + !Float.isNaN(mHeightOfTallestInlineViewOrImage) && + mHeightOfTallestInlineViewOrImage > mLineHeight; + return useInlineViewHeight ? mHeightOfTallestInlineViewOrImage : mLineHeight; } @Override - public void onBeforeLayout() { + public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { if (mIsVirtual) { return; } - mPreparedSpannableText = fromTextCSSNode(this); + mPreparedSpannableText = fromTextCSSNode(this, nativeViewHierarchyOptimizer); markUpdated(); } @@ -421,7 +470,7 @@ public void setColor(@Nullable Integer color) { @ReactProp(name = ViewProps.BACKGROUND_COLOR) public void setBackgroundColor(Integer color) { // Don't apply background color to anchor TextView since it will be applied on the View directly - if (!isVirtualAnchor()) { + if (isVirtual()) { mIsBackgroundColorSet = (color != null); if (mIsBackgroundColorSet) { mBackgroundColor = color; @@ -528,9 +577,18 @@ public void setTextShadowColor(int textShadowColor) { } } + protected boolean supportsInlineViews() { + return true; + } + + @Override + public boolean hoistNativeChildren() { + return supportsInlineViews(); + } + @Override public boolean isVirtualAnchor() { - return !mIsVirtual; + return !supportsInlineViews() && !mIsVirtual; } @Override @@ -550,4 +608,28 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); } } + + public static final Iterable NO_CSS_NODES = new ArrayList(0); + @Override + public Iterable calculateLayoutOnChildren(CSSLayoutContext layoutContext, UIViewOperationQueue viewOperationsQueue) { + // Run flexbox on and return the descendants which are inline views. + + if (mInlineViews == null || mInlineViews.isEmpty()) { + return NO_CSS_NODES; + } + + Spanned text = Assertions.assertNotNull( + this.mPreparedSpannableText, + "Spannable element has not been prepared in onBeforeLayout"); + TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(0, text.length(), TextInlineViewPlaceholderSpan.class); + ArrayList shadowNodes = new ArrayList(placeholders.length); + + for (TextInlineViewPlaceholderSpan placeholder : placeholders) { + ReactShadowNode child = mInlineViews.get(placeholder.getReactTag()); + child.calculateLayout(layoutContext); + shadowNodes.add(child); + } + + return shadowNodes; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 9ef3df91a8ab58..d541c76993943b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -14,11 +14,14 @@ import android.text.Layout; import android.text.Spanned; import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.facebook.csslayout.FloatUtil; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.ReactCompoundView; +import com.facebook.react.uimanager.UIManagerModule; public class ReactTextView extends TextView implements ReactCompoundView { @@ -38,6 +41,69 @@ public ReactTextView(Context context) { mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; } + @Override + protected void onLayout(boolean changed, + int textViewLeft, + int textViewTop, + int textViewRight, + int textViewBottom) { + if (!(getText() instanceof Spanned)) { + /** + * In general, {@link #setText} is called via {@link ReactTextViewManager#updateExtraData} + * before we are laid out. This ordering is a requirement because we utilize the data from + * setText in onLayout. + * + * However, it's possible for us to get an extra layout before we've received our setText + * call. If this happens before the initial setText call, then getText() will have its default + * value which isn't a Spanned and we need to bail out. That's fine because we'll get a + * setText followed by a layout later. + * + * The cause for the extra early layout is that an ancestor gets transitioned from a + * layout-only node to a non layout-only node. + */ + return; + } + + UIManagerModule uiManager = ((ReactContext) getContext()).getNativeModule(UIManagerModule.class); + + Spanned text = (Spanned) getText(); + Layout layout = getLayout(); + TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(0, text.length(), TextInlineViewPlaceholderSpan.class); + int textViewWidth = textViewRight - textViewLeft; + int textViewHeight = textViewBottom - textViewTop; + + for (TextInlineViewPlaceholderSpan placeholder : placeholders) { + View child = uiManager.resolveView(placeholder.getReactTag()); + + int width = placeholder.getWidth(); + int height = placeholder.getHeight(); + int start = text.getSpanStart(placeholder); + int line = layout.getLineForOffset(start); + + int primaryHorizontal; + // There's a bug on Samsung devices where calling getPrimaryHorizontal on + // the last offset in the layout will result in an endless loop. Work around + // this bug by avoiding getPrimaryHorizontal in that case. + if (start == text.length() - 1) { + primaryHorizontal = (int) layout.getLineRight(line) - width; + } else { + primaryHorizontal = (int) layout.getPrimaryHorizontal(start); + } + + int leftRelativeToTextView = getTotalPaddingLeft() + primaryHorizontal; + int left = textViewLeft + leftRelativeToTextView; + int topRelativeToTextView = getTotalPaddingTop() + layout.getLineBaseline(line) - height; + int top = textViewTop + topRelativeToTextView; + + boolean isLineTruncated = layout.getEllipsisCount(line) > 0; + boolean isFullyClipped = (isLineTruncated && start >= layout.getEllipsisStart(line)) || + textViewWidth <= leftRelativeToTextView || textViewHeight <= topRelativeToTextView; + child.setVisibility(isFullyClipped ? View.GONE : View.VISIBLE); + + child.layout(left, top, left + width, top + height); + } + } + public void setText(ReactTextUpdate update) { mContainsImages = update.containsImages(); // Android's TextView crashes when it tries to relayout if LayoutParams are @@ -62,6 +128,9 @@ public void setText(ReactTextUpdate update) { setLineSpacing(mLineHeight, 0); } } + + // Ensure onLayout is called so the inline views can be repositioned. + requestLayout(); } @Override 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 dc200c1fcb7a74..bfcbf118cfd479 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 @@ -18,6 +18,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.IViewManagerWithChildren; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; @@ -33,7 +34,8 @@ * @{link ReactTextShadowNode} hierarchy to calculate a {@link Spannable} text representing the * whole text subtree. */ -public class ReactTextViewManager extends BaseViewManager { +public class ReactTextViewManager extends BaseViewManager + implements IViewManagerWithChildren { @VisibleForTesting public static final String REACT_CLASS = "RCTText"; @@ -127,4 +129,9 @@ public ReactTextShadowNode createShadowNodeInstance() { public Class getShadowNodeClass() { return ReactTextShadowNode.class; } + + @Override + public boolean needsCustomLayoutForChildren() { + return true; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineViewPlaceholderSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineViewPlaceholderSpan.java new file mode 100644 index 00000000000000..76223b39233c61 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextInlineViewPlaceholderSpan.java @@ -0,0 +1,63 @@ +/** + * 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.views.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.style.ReplacementSpan; + +/** + * TextInlineViewPlaceholderSpan is a span for inlined views that are inside . It computes + * its size based on the input size. It contains no draw logic, just positioning logic. + */ +public class TextInlineViewPlaceholderSpan extends ReplacementSpan { + private int mReactTag; + private int mWidth; + private int mHeight; + + public TextInlineViewPlaceholderSpan(int reactTag, int width, int height) { + mReactTag = reactTag; + mWidth = width; + mHeight = height; + } + + public int getReactTag() { + return mReactTag; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + @Override + public int getSize( + Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + // NOTE: This getSize code is copied from DynamicDrawableSpan and modified to not use a Drawable + + if (fm != null) { + fm.ascent = -mHeight; + fm.descent = 0; + + fm.top = fm.ascent; + fm.bottom = 0; + } + + return mWidth; + } + + @Override + public void draw( + Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index cc71f6b88c13b0..72ba1c1172e36a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -22,6 +22,7 @@ import com.facebook.csslayout.Spacing; import com.facebook.infer.annotation.Assertions; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIViewOperationQueue; @@ -44,6 +45,11 @@ public ReactTextInputShadowNode() { setMeasureFunction(this); } + @Override + public boolean supportsInlineViews() { + return false; + } + @Override public void setThemedContext(ThemedReactContext themedContext) { super.setThemedContext(themedContext); @@ -98,7 +104,7 @@ public void measure( } @Override - public void onBeforeLayout() { + public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { // We don't have to measure the text within the text input. return; } @@ -117,7 +123,7 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { } if (mJsEventCount != UNSET) { - Spannable preparedSpannableText = fromTextCSSNode(this); + Spannable preparedSpannableText = fromTextCSSNode(this, null); ReactTextUpdate reactTextUpdate = new ReactTextUpdate(preparedSpannableText, mJsEventCount, mContainsImages, getPadding(), getEffectiveLineHeight()); uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); diff --git a/docs/Text.md b/docs/Text.md index 86537f8a20d07b..56319e77f8f463 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -32,9 +32,9 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or 9-17: bold, red ``` -## Nested Views (iOS Only) +## Nested Views -On iOS, you can nest views within your Text component. Here's an example: +You can nest views within your Text component. Here's an example: ```ReactNativeWebPlayer import React, { Component } from 'react';