Skip to content

Commit 00d7bb1

Browse files
author
Adam Comella
committed
Android: Enable views to be nested within <Text>
Android version of this iOS PR: facebook#7304 Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported via FrescoBasedReactTextInlineImageSpan. To get support for nesting views within Text, we create one special kind of span per inline view. This span is called TextInlineViewPlaceholderSpan. It is the same size as the inline view. It's job is just to occupy space -- it doesn't render any visual. After the text is rendered, we query the Android Layout associated with the TextView to find out where it has positioned each TextInlineViewPlaceholderSpan. We then position the views to be at those locations. Limitation: Clipping ==== If Text's height/width is small such that an inline view doesn't completely fit, the inline view may still be fully visible due to hoisting (the inline view isn't actually parented to the Text which has the limited size. It is parented to an ancestor which may have a different clipping rectangle.). Prior to this change, layout-only views had a similar limitation.
1 parent 2fc87f6 commit 00d7bb1

File tree

10 files changed

+397
-99
lines changed

10 files changed

+397
-99
lines changed

ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
// NOTE: this file is auto-copied from https://github.com/facebook/css-layout
1010
// @generated SignedSource<<da35a9f6c5a59af0d73da3e46ee60a9a>>
1111

12+
// NOTE: Changes in this file must be imported from this css-layout PR:
13+
1214
package com.facebook.csslayout;
1315

1416
import javax.annotation.Nullable;
@@ -26,7 +28,7 @@
2628

2729
/**
2830
* A CSS Node. It has a style object you can manipulate at {@link #style}. After calling
29-
* {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout.
31+
* {@link #calculateLayout(CSSLayoutContext, float, float)}, {@link #layout} will be filled with the results of the layout.
3032
*/
3133
public class CSSNode {
3234

@@ -74,6 +76,11 @@ public static interface MeasureFunction {
7476
private LayoutState mLayoutState = LayoutState.DIRTY;
7577
private boolean mIsTextNode = false;
7678

79+
public static final Iterable<CSSNode> NO_CSS_NODES = new ArrayList<CSSNode>(0);
80+
public Iterable<CSSNode> getChildrenIterable() {
81+
return mChildren == null ? NO_CSS_NODES : mChildren;
82+
}
83+
7784
public int getChildCount() {
7885
return mChildren == null ? 0 : mChildren.size();
7986
}
@@ -149,8 +156,8 @@ public boolean isTextNode() {
149156
/**
150157
* Performs the actual layout and saves the results in {@link #layout}
151158
*/
152-
public void calculateLayout(CSSLayoutContext layoutContext) {
153-
LayoutEngine.layoutNode(layoutContext, this, CSSConstants.UNDEFINED, CSSConstants.UNDEFINED, null);
159+
public void calculateLayout(CSSLayoutContext layoutContext, float availableWidth, float availableHeight) {
160+
LayoutEngine.layoutNode(layoutContext, this, availableWidth, availableHeight, null);
154161
}
155162

156163
/**
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.uimanager;
11+
12+
public enum NativeKind {
13+
// Node is in the native hierarchy and the HierarchyOptimizer should assume it can host children
14+
// (e.g. because it's a ViewGroup). Note that it's okay if the node doesn't support children. When
15+
// the HierarchyOptimizer generates children manipulation commands for that node, the
16+
// HierarchyManager will catch this case and throw an exception.
17+
PARENT,
18+
// Node is in the native hierarchy, it may have children, but it cannot host them itself (e.g.
19+
// because it isn't a ViewGroup). Consequently, its children need to be hosted by an ancestor.
20+
LEAF,
21+
// Node is not in the native hierarchy.
22+
NONE
23+
}

ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ private static class NodeIndexPair {
6868
private final ShadowNodeRegistry mShadowNodeRegistry;
6969
private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray();
7070

71+
public static void assertNodeSupportedWithoutOptimizer(ReactShadowNode node) {
72+
// NativeKind.LEAF nodes require the optimizer. They are not ViewGroups so they cannot host
73+
// their native children themselves. Their native children need to be hoisted by the optimizer
74+
// to an ancestor which is a ViewGroup.
75+
Assertions.assertCondition(
76+
node.getNativeKind() != NativeKind.LEAF,
77+
"Nodes with NativeKind.LEAF are not supported when the optimizer is disabled");
78+
}
79+
7180
public NativeViewHierarchyOptimizer(
7281
UIViewOperationQueue uiViewOperationQueue,
7382
ShadowNodeRegistry shadowNodeRegistry) {
@@ -83,6 +92,7 @@ public void handleCreateView(
8392
ThemedReactContext themedContext,
8493
@Nullable ReactStylesDiffMap initialProps) {
8594
if (!ENABLED) {
95+
assertNodeSupportedWithoutOptimizer(node);
8696
int tag = node.getReactTag();
8797
mUIViewOperationQueue.enqueueCreateView(
8898
themedContext,
@@ -96,7 +106,7 @@ public void handleCreateView(
96106
isLayoutOnlyAndCollapsable(initialProps);
97107
node.setIsLayoutOnly(isLayoutOnly);
98108

99-
if (!isLayoutOnly) {
109+
if (node.getNativeKind() != NativeKind.NONE) {
100110
mUIViewOperationQueue.enqueueCreateView(
101111
themedContext,
102112
node.getReactTag(),
@@ -122,6 +132,7 @@ public void handleUpdateView(
122132
String className,
123133
ReactStylesDiffMap props) {
124134
if (!ENABLED) {
135+
assertNodeSupportedWithoutOptimizer(node);
125136
mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
126137
return;
127138
}
@@ -151,6 +162,7 @@ public void handleManageChildren(
151162
ViewAtIndex[] viewsToAdd,
152163
int[] tagsToDelete) {
153164
if (!ENABLED) {
165+
assertNodeSupportedWithoutOptimizer(nodeToManage);
154166
mUIViewOperationQueue.enqueueManageChildren(
155167
nodeToManage.getReactTag(),
156168
indicesToRemove,
@@ -191,6 +203,7 @@ public void handleSetChildren(
191203
ReadableArray childrenTags
192204
) {
193205
if (!ENABLED) {
206+
assertNodeSupportedWithoutOptimizer(nodeToManage);
194207
mUIViewOperationQueue.enqueueSetChildren(
195208
nodeToManage.getReactTag(),
196209
childrenTags);
@@ -210,6 +223,7 @@ public void handleSetChildren(
210223
*/
211224
public void handleUpdateLayout(ReactShadowNode node) {
212225
if (!ENABLED) {
226+
assertNodeSupportedWithoutOptimizer(node);
213227
mUIViewOperationQueue.enqueueUpdateLayout(
214228
Assertions.assertNotNull(node.getParent()).getReactTag(),
215229
node.getReactTag(),
@@ -223,6 +237,12 @@ public void handleUpdateLayout(ReactShadowNode node) {
223237
applyLayoutBase(node);
224238
}
225239

240+
public void handleForceViewToBeNonLayoutOnly(ReactShadowNode node) {
241+
if (node.isLayoutOnly()) {
242+
transitionLayoutOnlyViewToNativeView(node, null);
243+
}
244+
}
245+
226246
/**
227247
* Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native
228248
* hierarchy. Should be called after all updateLayout calls for a batch have been handled.
@@ -231,16 +251,18 @@ public void onBatchComplete() {
231251
mTagsWithLayoutVisited.clear();
232252
}
233253

234-
private NodeIndexPair walkUpUntilNonLayoutOnly(
254+
private NodeIndexPair walkUpUntilNativeKindIsParent(
235255
ReactShadowNode node,
236256
int indexInNativeChildren) {
237-
while (node.isLayoutOnly()) {
257+
while (node.getNativeKind() != NativeKind.PARENT) {
238258
ReactShadowNode parent = node.getParent();
239259
if (parent == null) {
240260
return null;
241261
}
242262

243-
indexInNativeChildren = indexInNativeChildren + parent.getNativeOffsetForChild(node);
263+
indexInNativeChildren = indexInNativeChildren +
264+
(node.getNativeKind() == NativeKind.LEAF ? 1 : 0) +
265+
parent.getNativeOffsetForChild(node);
244266
node = parent;
245267
}
246268

@@ -249,8 +271,8 @@ private NodeIndexPair walkUpUntilNonLayoutOnly(
249271

250272
private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) {
251273
int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index));
252-
if (parent.isLayoutOnly()) {
253-
NodeIndexPair result = walkUpUntilNonLayoutOnly(parent, indexInNativeChildren);
274+
if (parent.getNativeKind() != NativeKind.PARENT) {
275+
NodeIndexPair result = walkUpUntilNativeKindIsParent(parent, indexInNativeChildren);
254276
if (result == null) {
255277
// If the parent hasn't been attached to its native parent yet, don't issue commands to the
256278
// 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
260282
indexInNativeChildren = result.index;
261283
}
262284

263-
if (!child.isLayoutOnly()) {
264-
addNonLayoutNode(parent, child, indexInNativeChildren);
285+
if (child.getNativeKind() != NativeKind.NONE) {
286+
addNativeChild(parent, child, indexInNativeChildren);
265287
} else {
266-
addLayoutOnlyNode(parent, child, indexInNativeChildren);
288+
addNonNativeChild(parent, child, indexInNativeChildren);
267289
}
268290
}
269291

270292
/**
271-
* For handling node removal from manageChildren. In the case of removing a layout-only node, we
272-
* need to instead recursively remove all its children from their native parents.
293+
* For handling node removal from manageChildren. In the case of removing a node which isn't
294+
* hosting its own children (e.g. layout-only or NativeKind.LEAF), we need to recursively remove
295+
* all its children from their native parents.
273296
*/
274297
private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) {
275-
ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();
298+
if (nodeToRemove.getNativeKind() != NativeKind.PARENT) {
299+
for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
300+
removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
301+
}
302+
}
276303

304+
ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();
277305
if (nativeNodeToRemoveFrom != null) {
278306
int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove);
279307
nativeNodeToRemoveFrom.removeNativeChildAt(index);
@@ -283,21 +311,17 @@ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDe
283311
new int[]{index},
284312
null,
285313
shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null);
286-
} else {
287-
for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
288-
removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
289-
}
290314
}
291315
}
292316

293-
private void addLayoutOnlyNode(
294-
ReactShadowNode nonLayoutOnlyNode,
295-
ReactShadowNode layoutOnlyNode,
317+
private void addNonNativeChild(
318+
ReactShadowNode nativeParent,
319+
ReactShadowNode nonNativeChild,
296320
int index) {
297-
addGrandchildren(nonLayoutOnlyNode, layoutOnlyNode, index);
321+
addGrandchildren(nativeParent, nonNativeChild, index);
298322
}
299323

300-
private void addNonLayoutNode(
324+
private void addNativeChild(
301325
ReactShadowNode parent,
302326
ReactShadowNode child,
303327
int index) {
@@ -307,30 +331,33 @@ private void addNonLayoutNode(
307331
null,
308332
new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)},
309333
null);
334+
335+
if (child.getNativeKind() != NativeKind.PARENT) {
336+
addGrandchildren(parent, child, index + 1);
337+
}
310338
}
311339

312340
private void addGrandchildren(
313341
ReactShadowNode nativeParent,
314342
ReactShadowNode child,
315343
int index) {
316-
Assertions.assertCondition(!nativeParent.isLayoutOnly());
344+
Assertions.assertCondition(child.getNativeKind() != NativeKind.PARENT);
317345

318346
// `child` can't hold native children. Add all of `child`'s children to `parent`.
319347
int currentIndex = index;
320348
for (int i = 0; i < child.getChildCount(); i++) {
321349
ReactShadowNode grandchild = child.getChildAt(i);
322350
Assertions.assertCondition(grandchild.getNativeParent() == null);
323351

324-
if (grandchild.isLayoutOnly()) {
325-
// Adding this child could result in adding multiple native views
326-
int grandchildCountBefore = nativeParent.getNativeChildCount();
327-
addLayoutOnlyNode(nativeParent, grandchild, currentIndex);
328-
int grandchildCountAfter = nativeParent.getNativeChildCount();
329-
currentIndex += grandchildCountAfter - grandchildCountBefore;
352+
// Adding this child could result in adding multiple native views
353+
int grandchildCountBefore = nativeParent.getNativeChildCount();
354+
if (grandchild.getNativeKind() == NativeKind.NONE) {
355+
addNonNativeChild(nativeParent, grandchild, currentIndex);
330356
} else {
331-
addNonLayoutNode(nativeParent, grandchild, currentIndex);
332-
currentIndex++;
357+
addNativeChild(nativeParent, grandchild, currentIndex);
333358
}
359+
int grandchildCountAfter = nativeParent.getNativeChildCount();
360+
currentIndex += grandchildCountAfter - grandchildCountBefore;
334361
}
335362
}
336363

@@ -349,7 +376,7 @@ private void applyLayoutBase(ReactShadowNode node) {
349376
int x = node.getScreenX();
350377
int y = node.getScreenY();
351378

352-
while (parent != null && parent.isLayoutOnly()) {
379+
while (parent != null && parent.getNativeKind() != NativeKind.PARENT) {
353380
// TODO(7854667): handle and test proper clipping
354381
x += Math.round(parent.getLayoutX());
355382
y += Math.round(parent.getLayoutY());
@@ -361,7 +388,7 @@ private void applyLayoutBase(ReactShadowNode node) {
361388
}
362389

363390
private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) {
364-
if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) {
391+
if (toUpdate.getNativeKind() != NativeKind.NONE && toUpdate.getNativeParent() != null) {
365392
int tag = toUpdate.getReactTag();
366393
mUIViewOperationQueue.enqueueUpdateLayout(
367394
toUpdate.getNativeParent().getReactTag(),

0 commit comments

Comments
 (0)