mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Android: Enable views to be nested within <Text> (#23195)
Summary: Potential breaking change: The signature of ReactShadowNode's onBeforeLayout method was changed - Before: public void onBeforeLayout() - After: public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) Implements same feature as this iOS PR: https://github.com/facebook/react-native/pull/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. Its job is just to occupy space -- it doesn't render any visual. After the text is rendered, we query the Android Layout object associated with the TextView to find out where it has positioned each TextInlineViewPlaceholderSpan. We then position the views to be at those locations. One tricky aspect of the implementation is that the Text component needs to be able to render native children (the inline views) but the Android TextView cannot have children. This is solved by having the native parent of the ReactTextView also host the inline views. Implementation-wise, this was accomplished by extending the NativeViewHierarchyOptimizer to handle this case. The optimizer now handles these cases: - Node is not in the native tree. An ancestor must host its children. - Node is in the native tree and it can host its own children. - (new) Node is in the native tree but it cannot host its own children. An ancestor must host both this node and its children. I added the `onInlineViewLayout` event which is useful for writing tests for verifying that the inline views are positioned properly. 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. Pull Request resolved: https://github.com/facebook/react-native/pull/23195 Differential Revision: D14014668 Pulled By: shergin fbshipit-source-id: d46130f3d19cc83ac7ddf423adcc9e23988245d3
This commit is contained in:
committed by
Facebook Github Bot
parent
770da3ac67
commit
a2285b1790
@@ -67,6 +67,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
private boolean mNodeUpdated = true;
|
||||
private @Nullable ArrayList<ReactShadowNodeImpl> mChildren;
|
||||
private @Nullable ReactShadowNodeImpl mParent;
|
||||
private @Nullable ReactShadowNodeImpl mLayoutParent;
|
||||
|
||||
// layout-only nodes
|
||||
private boolean mIsLayoutOnly;
|
||||
@@ -98,7 +99,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
|
||||
/**
|
||||
* Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not
|
||||
* mapped into native views (e.g. nested text node). By default this method returns {@code false}.
|
||||
* mapped into native views or Yoga nodes (e.g. nested text node). By default this method returns
|
||||
* {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean isVirtual() {
|
||||
@@ -107,9 +109,9 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Override
|
||||
public boolean isVirtualAnchor() {
|
||||
@@ -127,6 +129,17 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return isMeasureDefined();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
@Override
|
||||
public boolean hoistNativeChildren() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getViewClass() {
|
||||
return Assertions.assertNotNull(mViewClassName);
|
||||
@@ -166,6 +179,18 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
public void dirty() {
|
||||
if (!isVirtual()) {
|
||||
mYogaNode.dirty();
|
||||
} else if (getParent() != null) {
|
||||
// Virtual nodes aren't involved in layout but they need to have the dirty signal
|
||||
// propagated to their ancestors.
|
||||
//
|
||||
// TODO: There are some edge cases that currently aren't supported. For example, if the size
|
||||
// of your inline image/view changes, its size on-screen is not be updated. Similarly,
|
||||
// if the size of a view inside of an inline view changes, its size on-screen is not
|
||||
// updated. The problem may be that dirty propagation stops at inline views because the
|
||||
// parent of each inline view is null. A possible fix would be to implement an `onDirty`
|
||||
// handler in Yoga that will propagate the dirty signal to the ancestors of the inline view.
|
||||
//
|
||||
getParent().dirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +224,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
markUpdated();
|
||||
|
||||
int increase = child.isLayoutOnly() ? child.getTotalNativeChildren() : 1;
|
||||
int increase = child.getTotalNativeNodeContributionToParent();
|
||||
mTotalNativeChildren += increase;
|
||||
|
||||
updateNativeChildrenCountInParent(increase);
|
||||
@@ -219,7 +244,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
markUpdated();
|
||||
|
||||
int decrease = removed.isLayoutOnly() ? removed.getTotalNativeChildren() : 1;
|
||||
int decrease = removed.getTotalNativeNodeContributionToParent();
|
||||
mTotalNativeChildren -= decrease;
|
||||
updateNativeChildrenCountInParent(-decrease);
|
||||
return removed;
|
||||
@@ -257,9 +282,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
ReactShadowNodeImpl toRemove = getChildAt(i);
|
||||
toRemove.mParent = null;
|
||||
decrease += toRemove.getTotalNativeNodeContributionToParent();
|
||||
toRemove.dispose();
|
||||
|
||||
decrease += toRemove.isLayoutOnly() ? toRemove.getTotalNativeChildren() : 1;
|
||||
}
|
||||
Assertions.assertNotNull(mChildren).clear();
|
||||
markUpdated();
|
||||
@@ -269,11 +293,11 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
}
|
||||
|
||||
private void updateNativeChildrenCountInParent(int delta) {
|
||||
if (mIsLayoutOnly) {
|
||||
if (getNativeKind() != NativeKind.PARENT) {
|
||||
ReactShadowNodeImpl parent = getParent();
|
||||
while (parent != null) {
|
||||
parent.mTotalNativeChildren += delta;
|
||||
if (!parent.isLayoutOnly()) {
|
||||
if (parent.getNativeKind() == NativeKind.PARENT) {
|
||||
break;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
@@ -287,7 +311,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
* require layouting (marked with {@link #dirty()}).
|
||||
*/
|
||||
@Override
|
||||
public void onBeforeLayout() {}
|
||||
public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void updateProperties(ReactStylesDiffMap props) {
|
||||
@@ -397,6 +422,17 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return mParent;
|
||||
}
|
||||
|
||||
// Returns the node that is responsible for laying out this node.
|
||||
@Override
|
||||
public final @Nullable ReactShadowNodeImpl getLayoutParent() {
|
||||
return mLayoutParent != null ? mLayoutParent : getNativeParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setLayoutParent(@Nullable ReactShadowNodeImpl layoutParent) {
|
||||
mLayoutParent = layoutParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ThemedReactContext} associated with this {@link ReactShadowNodeImpl}. This will
|
||||
* never change during the lifetime of a {@link ReactShadowNodeImpl} instance, but different
|
||||
@@ -446,8 +482,8 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
*/
|
||||
@Override
|
||||
public final void addNativeChildAt(ReactShadowNodeImpl 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);
|
||||
@@ -508,6 +544,14 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return mIsLayoutOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeKind getNativeKind() {
|
||||
return
|
||||
isVirtual() || isLayoutOnly() ? NativeKind.NONE :
|
||||
hoistNativeChildren() ? NativeKind.LEAF :
|
||||
NativeKind.PARENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getTotalNativeChildren() {
|
||||
return mTotalNativeChildren;
|
||||
@@ -531,6 +575,14 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
return isDescendant;
|
||||
}
|
||||
|
||||
private int getTotalNativeNodeContributionToParent() {
|
||||
NativeKind kind = getNativeKind();
|
||||
return
|
||||
kind == NativeKind.NONE ? mTotalNativeChildren :
|
||||
kind == NativeKind.LEAF ? 1 + mTotalNativeChildren :
|
||||
1; // kind == NativeKind.PARENT
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + mViewClassName + " " + getReactTag() + "]";
|
||||
@@ -585,7 +637,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
index += (current.isLayoutOnly() ? current.getTotalNativeChildren() : 1);
|
||||
index += current.getTotalNativeNodeContributionToParent();
|
||||
}
|
||||
if (!found) {
|
||||
throw new RuntimeException(
|
||||
@@ -978,4 +1030,13 @@ public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl>
|
||||
public Integer getHeightMeasureSpec() {
|
||||
return mHeightMeasureSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends ReactShadowNode> calculateLayoutOnChildren() {
|
||||
return isVirtualAnchor() ?
|
||||
// All of the descendants are virtual so none of them are involved in layout.
|
||||
null :
|
||||
// Just return the children. Flexbox calculations have already been run on them.
|
||||
mChildren;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user