Files
react-native/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java
Andy Street 1d555bff22 BREAKING [react_native/css_layout] Update RN shadow nodes to hold CSSNode instead of extending CSSNode
Summary:
@public

This diff makes it so ReactShadowNode holds a CSSNode instead of extending one. This will enable us to pool and re-use CSSNodes and will allow us to keep from breaking the CSSNode api assumption that nodes that have measure functions don't have children (right now, text nodes have measure functions, but they also have raw text children).

BREAKING
This diff makes ReactShadowNode no longer extend CSSNodeDEPRECATED. If you have code that depended on that, e.g. via instanceof checks, that will no longer work as expected. Subclasses that override getChildAt/addChildAt/etc will need to update your method signatures. There should be no runtime behavior changes.

Reviewed By: emilsjolander

Differential Revision: D4153818
2016-12-19 13:40:35 -08:00

562 lines
18 KiB
Java

/**
* 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 javax.annotation.Nullable;
import android.graphics.Rect;
import com.facebook.csslayout.CSSNodeDEPRECATED;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.OnLayoutEvent;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
/**
* FlatShadowNode is a base class for all shadow node used in FlatUIImplementation. It extends
* {@link LayoutShadowNode} by adding an ability to prepare DrawCommands off the UI thread.
*/
/* package */ class FlatShadowNode extends LayoutShadowNode {
/* package */ static final FlatShadowNode[] EMPTY_ARRAY = new FlatShadowNode[0];
private static final String PROP_DECOMPOSED_MATRIX = "decomposedMatrix";
private static final String PROP_OPACITY = "opacity";
private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid";
private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel";
private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType";
private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
private static final String PROP_TEST_ID = "testID";
private static final String PROP_TRANSFORM = "transform";
protected static final String PROP_REMOVE_CLIPPED_SUBVIEWS =
ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS;
protected static final String PROP_HORIZONTAL = "horizontal";
private static final Rect LOGICAL_OFFSET_EMPTY = new Rect();
// When we first initialize a backing view, we create a view we are going to throw away anyway,
// so instead initialize with a shared view.
private static final DrawView EMPTY_DRAW_VIEW = new DrawView(0);
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY;
private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY;
private FlatShadowNode[] mNativeChildren = FlatShadowNode.EMPTY_ARRAY;
private NodeRegion mNodeRegion = NodeRegion.EMPTY;
private int mNativeParentTag;
private int mViewLeft;
private int mViewTop;
private int mViewRight;
private int mViewBottom;
private boolean mBackingViewIsCreated;
private @Nullable DrawView mDrawView;
private @Nullable DrawBackgroundColor mDrawBackground;
private boolean mIsUpdated = true;
private boolean mForceMountChildrenToView;
private float mClipLeft;
private float mClipTop;
private float mClipRight;
private float mClipBottom;
// Used to track whether any of the NodeRegions overflow this Node. This is used to determine
// whether or not we can detach this Node in the context of a container with
// setRemoveClippedSubviews enabled.
private boolean mOverflowsContainer;
// this Rect contains the offset to get the "logical bounds" (i.e. bounds that include taking
// into account overflow visible).
private Rect mLogicalOffset = LOGICAL_OFFSET_EMPTY;
// last OnLayoutEvent info, only used when shouldNotifyOnLayout() is true.
private int mLayoutX;
private int mLayoutY;
private int mLayoutWidth;
private int mLayoutHeight;
// clip radius
float mClipRadius;
boolean mClipToBounds = false;
/* package */ void handleUpdateProperties(ReactStylesDiffMap styles) {
if (!mountsToView()) {
// Make sure we mount this FlatShadowNode to a View if any of these properties are present.
if (styles.hasKey(PROP_DECOMPOSED_MATRIX) ||
styles.hasKey(PROP_OPACITY) ||
styles.hasKey(PROP_RENDER_TO_HARDWARE_TEXTURE) ||
styles.hasKey(PROP_TEST_ID) ||
styles.hasKey(PROP_ACCESSIBILITY_LABEL) ||
styles.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE) ||
styles.hasKey(PROP_ACCESSIBILITY_LIVE_REGION) ||
styles.hasKey(PROP_TRANSFORM) ||
styles.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY) ||
styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) {
forceMountToView();
}
}
}
/* package */ final void forceMountChildrenToView() {
if (mForceMountChildrenToView) {
return;
}
mForceMountChildrenToView = true;
for (int i = 0, childCount = getChildCount(); i != childCount; ++i) {
ReactShadowNode child = getChildAt(i);
if (child instanceof FlatShadowNode) {
((FlatShadowNode) child).forceMountToView();
}
}
}
/**
* Collects DrawCommands produced by this FlatShadowNode.
*/
protected void collectState(
StateBuilder stateBuilder,
float left,
float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
if (mDrawBackground != null) {
mDrawBackground = (DrawBackgroundColor) mDrawBackground.updateBoundsAndFreeze(
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
stateBuilder.addDrawCommand(mDrawBackground);
}
}
@ReactProp(name = ViewProps.BACKGROUND_COLOR)
public void setBackgroundColor(int backgroundColor) {
mDrawBackground = (backgroundColor == 0) ? null : new DrawBackgroundColor(backgroundColor);
invalidate();
}
@Override
public void setOverflow(String overflow) {
super.setOverflow(overflow);
mClipToBounds = "hidden".equals(overflow);
if (mClipToBounds) {
mOverflowsContainer = false;
if (mClipRadius > DrawView.MINIMUM_ROUNDED_CLIPPING_VALUE) {
// mount to a view if we are overflow: hidden and are clipping, so that we can do one
// clipPath to clip all the children of this node (both DrawCommands and Views).
forceMountToView();
}
} else {
updateOverflowsContainer();
}
invalidate();
}
public final boolean clipToBounds() {
return mClipToBounds;
}
@Override
public final int getScreenX() {
return mViewLeft;
}
@Override
public final int getScreenY() {
return mViewTop;
}
@Override
public final int getScreenWidth() {
if (mountsToView()) {
return mViewRight - mViewLeft;
} else {
return Math.round(mNodeRegion.getRight() - mNodeRegion.getLeft());
}
}
@Override
public final int getScreenHeight() {
if (mountsToView()) {
return mViewBottom - mViewTop;
} else {
return Math.round(mNodeRegion.getBottom() - mNodeRegion.getTop());
}
}
@Override
public void addChildAt(ReactShadowNode child, int i) {
super.addChildAt(child, i);
if (mForceMountChildrenToView && child instanceof FlatShadowNode) {
((FlatShadowNode) child).forceMountToView();
}
}
/**
* Marks root node as updated to trigger a StateBuilder pass to collect DrawCommands for the node
* tree. Use it when FlatShadowNode is updated but doesn't require a layout pass (e.g. background
* color is changed).
*/
protected final void invalidate() {
FlatShadowNode node = this;
while (true) {
if (node.mountsToView()) {
if (node.mIsUpdated) {
// already updated
return;
}
node.mIsUpdated = true;
}
ReactShadowNode parent = node.getParent();
if (parent == null) {
// not attached to a hierarchy yet
return;
}
node = (FlatShadowNode) parent;
}
}
@Override
public void markUpdated() {
super.markUpdated();
mIsUpdated = true;
invalidate();
}
/* package */ final boolean isUpdated() {
return mIsUpdated;
}
/* package */ final void resetUpdated() {
mIsUpdated = false;
}
/* package */ final boolean clipBoundsChanged(
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
return mClipLeft != clipLeft || mClipTop != clipTop ||
mClipRight != clipRight || mClipBottom != clipBottom;
}
/* package */ final void setClipBounds(
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
mClipLeft = clipLeft;
mClipTop = clipTop;
mClipRight = clipRight;
mClipBottom = clipBottom;
}
/**
* Returns an array of DrawCommands to perform during the View's draw pass.
*/
/* package */ final DrawCommand[] getDrawCommands() {
return mDrawCommands;
}
/**
* Sets an array of DrawCommands to perform during the View's draw pass. StateBuilder uses old
* draw commands to compare to new draw commands and see if the View needs to be redrawn.
*/
/* package */ final void setDrawCommands(DrawCommand[] drawCommands) {
mDrawCommands = drawCommands;
}
/**
* Sets an array of AttachDetachListeners to call onAttach/onDetach when they are attached to or
* detached from a View that this shadow node maps to.
*/
/* package */ final void setAttachDetachListeners(AttachDetachListener[] listeners) {
mAttachDetachListeners = listeners;
}
/**
* Returns an array of AttachDetachListeners associated with this shadow node.
*/
/* package */ final AttachDetachListener[] getAttachDetachListeners() {
return mAttachDetachListeners;
}
/* package */ final FlatShadowNode[] getNativeChildren() {
return mNativeChildren;
}
/* package */ final void setNativeChildren(FlatShadowNode[] nativeChildren) {
mNativeChildren = nativeChildren;
}
/* package */ final int getNativeParentTag() {
return mNativeParentTag;
}
/* package */ final void setNativeParentTag(int nativeParentTag) {
mNativeParentTag = nativeParentTag;
}
/* package */ final NodeRegion[] getNodeRegions() {
return mNodeRegions;
}
/* package */ final void setNodeRegions(NodeRegion[] nodeRegion) {
mNodeRegions = nodeRegion;
updateOverflowsContainer();
}
/* package */ final void updateOverflowsContainer() {
boolean overflowsContainer = false;
int width = (int) (mNodeRegion.getRight() - mNodeRegion.getLeft());
int height = (int) (mNodeRegion.getBottom() - mNodeRegion.getTop());
float leftBound = 0;
float rightBound = width;
float topBound = 0;
float bottomBound = height;
Rect logicalOffset = null;
// when we are overflow:visible, we try to figure out if any of the children are outside
// of the bounds of this view. since NodeRegion bounds are relative to their parent (i.e.
// 0, 0 is always the start), we see how much outside of the bounds we are (negative left
// or top, or bottom that's more than height or right that's more than width). we set these
// offsets in mLogicalOffset for being able to more intelligently determine whether or not
// to clip certain subviews.
if (!mClipToBounds && height > 0 && width > 0) {
for (NodeRegion region : mNodeRegions) {
if (region.getLeft() < leftBound) {
leftBound = region.getLeft();
overflowsContainer = true;
}
if (region.getRight() > rightBound) {
rightBound = region.getRight();
overflowsContainer = true;
}
if (region.getTop() < topBound) {
topBound = region.getTop();
overflowsContainer = true;
}
if (region.getBottom() > bottomBound) {
bottomBound = region.getBottom();
overflowsContainer = true;
}
}
if (overflowsContainer) {
logicalOffset = new Rect(
(int) leftBound,
(int) topBound,
(int) (rightBound - width),
(int) (bottomBound - height));
}
}
// if we don't overflow, let's check if any of the immediate children overflow.
// this is "indirectly recursive," since this method is called when setNodeRegions is called,
// and the children call setNodeRegions before their parent. consequently, when a node deep
// inside the tree overflows, its immediate parent has mOverflowsContainer set to true, and,
// by extension, so do all of its ancestors, sufficing here to only check the immediate
// child's mOverflowsContainer value instead of recursively asking if each child overflows its
// container.
if (!overflowsContainer && mNodeRegion != NodeRegion.EMPTY) {
int children = getChildCount();
for (int i = 0; i < children; i++) {
ReactShadowNode node = getChildAt(i);
if (node instanceof FlatShadowNode && ((FlatShadowNode) node).mOverflowsContainer) {
Rect childLogicalOffset = ((FlatShadowNode) node).mLogicalOffset;
if (logicalOffset == null) {
logicalOffset = new Rect();
}
// TODO: t11674025 - improve this - a grandparent may end up having smaller logical
// bounds than its children (because the grandparent's size may be larger than that of
// its child, so the grandchild overflows its parent but not its grandparent). currently,
// if a 100x100 view has a 5x5 view, and inside it has a 10x10 view, the inner most view
// overflows its parent but not its grandparent - the logical bounds on the grandparent
// will still be 5x5 (because they're inherited from the child's logical bounds). this
// has the effect of causing us to clip 5px later than we really have to.
logicalOffset.union(childLogicalOffset);
overflowsContainer = true;
}
}
}
// if things changed, notify the parent(s) about said changes - while in many cases, this will
// be extra work (since we process this for the parents after the children), in some cases,
// we may have no new node regions in the parent, but have a new node region in the child, and,
// as a result, the parent may not get the correct value for overflows container.
if (mOverflowsContainer != overflowsContainer) {
mOverflowsContainer = overflowsContainer;
mLogicalOffset = logicalOffset == null ? LOGICAL_OFFSET_EMPTY : logicalOffset;
}
}
/* package */ void updateNodeRegion(
float left,
float top,
float right,
float bottom,
boolean isVirtual) {
if (!mNodeRegion.matches(left, top, right, bottom, isVirtual)) {
setNodeRegion(new NodeRegion(left, top, right, bottom, getReactTag(), isVirtual));
}
}
protected final void setNodeRegion(NodeRegion nodeRegion) {
mNodeRegion = nodeRegion;
updateOverflowsContainer();
}
/* package */ final NodeRegion getNodeRegion() {
return mNodeRegion;
}
/**
* Sets boundaries of the View that this node maps to relative to the parent left/top coordinate.
*/
/* package */ final void setViewBounds(int left, int top, int right, int bottom) {
mViewLeft = left;
mViewTop = top;
mViewRight = right;
mViewBottom = bottom;
}
/**
* Left position of the View this node maps to relative to the parent View.
*/
/* package */ final int getViewLeft() {
return mViewLeft;
}
/**
* Top position of the View this node maps to relative to the parent View.
*/
/* package */ final int getViewTop() {
return mViewTop;
}
/**
* Right position of the View this node maps to relative to the parent View.
*/
/* package */ final int getViewRight() {
return mViewRight;
}
/**
* Bottom position of the View this node maps to relative to the parent View.
*/
/* package */ final int getViewBottom() {
return mViewBottom;
}
/* package */ final void forceMountToView() {
if (isVirtual()) {
return;
}
if (mDrawView == null) {
// Create a new DrawView, but we might not know our react tag yet, so set it to 0 in the
// meantime.
mDrawView = EMPTY_DRAW_VIEW;
invalidate();
// reset NodeRegion to allow it getting garbage-collected
mNodeRegion = NodeRegion.EMPTY;
}
}
/* package */ final DrawView collectDrawView(
float left,
float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
Assertions.assumeNotNull(mDrawView);
if (mDrawView == EMPTY_DRAW_VIEW) {
// This is the first time we have collected this DrawView, but we have to create a new
// DrawView anyway, as reactTag is final, and our DrawView instance is the static copy.
mDrawView = new DrawView(getReactTag());
}
// avoid path clipping if overflow: visible
float clipRadius = mClipToBounds ? mClipRadius : 0.0f;
// We have the correct react tag, but we may need a new copy with updated bounds. If the bounds
// match or were never set, the same view is returned.
mDrawView = mDrawView.collectDrawView(
left,
top,
right,
bottom,
left + mLogicalOffset.left,
top + mLogicalOffset.top,
right + mLogicalOffset.right,
bottom + mLogicalOffset.bottom,
clipLeft,
clipTop,
clipRight,
clipBottom,
clipRadius);
return mDrawView;
}
@Nullable
/* package */ final OnLayoutEvent obtainLayoutEvent(int x, int y, int width, int height) {
if (mLayoutX == x && mLayoutY == y && mLayoutWidth == width && mLayoutHeight == height) {
return null;
}
mLayoutX = x;
mLayoutY = y;
mLayoutWidth = width;
mLayoutHeight = height;
return OnLayoutEvent.obtain(getReactTag(), x, y, width, height);
}
/* package */ final boolean mountsToView() {
return mDrawView != null;
}
/* package */ final boolean isBackingViewCreated() {
return mBackingViewIsCreated;
}
/* package */ final void signalBackingViewIsCreated() {
mBackingViewIsCreated = true;
}
public boolean clipsSubviews() {
return false;
}
public boolean isHorizontal() {
return false;
}
}