/**
* 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.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_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_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);
}
}
/**
* Return whether or not this node draws anything
*
* This is used to decide whether or not to collect the NodeRegion for this node. This ensures
* that any FlatShadowNode that does not emit any DrawCommands should not bother handling touch
* (i.e. if it draws absolutely nothing, it is, for all intents and purposes, a layout only node).
*
* @return whether or not this is node draws anything
*/
boolean doesDraw() {
// if it mounts to view or draws a background, we can collect it - otherwise, no, unless a
// child suggests some alternative behavior
return mDrawView != null || mDrawBackground != null;
}
@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;
}
}