Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android: Enable views to be nested within <Text> #8619

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Examples/UIExplorer/js/TextExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Text>
</UIExplorerBlock>
<UIExplorerBlock title="Inline images">
<UIExplorerBlock title="Inline views">
<Text>
This text contains an inline image <Image source={require('./flux.png')}/>. Neat, huh?
This text contains an inline blue view <View style={{width: 25, height: 25, backgroundColor: 'steelblue'}} /> and
an inline image <Image source={require('./flux.png')} style={{width: 30, height: 11, resizeMode: 'cover'}}/>. Neat, huh?
</Text>
</UIExplorerBlock>
<UIExplorerBlock title="Text shadow">
Expand Down
12 changes: 11 additions & 1 deletion ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<<da35a9f6c5a59af0d73da3e46ee60a9a>>

// 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;
Expand All @@ -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 {

Expand Down Expand Up @@ -72,6 +77,11 @@ public static interface MeasureFunction {
private LayoutState mLayoutState = LayoutState.DIRTY;
private boolean mIsTextNode = false;

public static final Iterable<CSSNode> NO_CSS_NODES = new ArrayList<CSSNode>(0);
public Iterable<CSSNode> getChildrenIterable() {
return mChildren == null ? NO_CSS_NODES : mChildren;
}

public int getChildCount() {
return mChildren == null ? 0 : mChildren.size();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -83,6 +92,7 @@ public void handleCreateView(
ThemedReactContext themedContext,
@Nullable ReactStylesDiffMap initialProps) {
if (!ENABLED) {
assertNodeSupportedWithoutOptimizer(node);
int tag = node.getReactTag();
mUIViewOperationQueue.enqueueCreateView(
themedContext,
Expand All @@ -96,7 +106,7 @@ public void handleCreateView(
isLayoutOnlyAndCollapsable(initialProps);
node.setIsLayoutOnly(isLayoutOnly);

if (!isLayoutOnly) {
if (node.getNativeKind() != NativeKind.NONE) {
mUIViewOperationQueue.enqueueCreateView(
themedContext,
node.getReactTag(),
Expand All @@ -122,6 +132,7 @@ public void handleUpdateView(
String className,
ReactStylesDiffMap props) {
if (!ENABLED) {
assertNodeSupportedWithoutOptimizer(node);
mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
return;
}
Expand Down Expand Up @@ -151,6 +162,7 @@ public void handleManageChildren(
ViewAtIndex[] viewsToAdd,
int[] tagsToDelete) {
if (!ENABLED) {
assertNodeSupportedWithoutOptimizer(nodeToManage);
mUIViewOperationQueue.enqueueManageChildren(
nodeToManage.getReactTag(),
indicesToRemove,
Expand Down Expand Up @@ -191,6 +203,7 @@ public void handleSetChildren(
ReadableArray childrenTags
) {
if (!ENABLED) {
assertNodeSupportedWithoutOptimizer(nodeToManage);
mUIViewOperationQueue.enqueueSetChildren(
nodeToManage.getReactTag(),
childrenTags);
Expand All @@ -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(),
Expand All @@ -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.
Expand All @@ -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;
}

Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -307,30 +331,33 @@ 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;
for (int i = 0; i < child.getChildCount(); i++) {
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;
}
}

Expand All @@ -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());
Expand All @@ -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,
Expand Down
Loading