mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-03 22:48:25 +08:00
Summary: Add jni bindings for csslayout. First step in many of removing LayoutEngine.java and performing all layout in native. Reviewed By: lucasr Differential Revision: D3648793
546 lines
18 KiB
Java
546 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.CSSNode;
|
|
import com.facebook.csslayout.CSSNodeAPI;
|
|
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.views.view.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";
|
|
private static final String PROP_REMOVE_CLIPPED_SUBVIEWS =
|
|
ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS;
|
|
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();
|
|
}
|
|
|
|
@ReactProp(name = "overflow")
|
|
public final void setOverflow(String 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() {
|
|
return mViewRight - mViewLeft;
|
|
}
|
|
|
|
@Override
|
|
public final int getScreenHeight() {
|
|
return mViewBottom - mViewTop;
|
|
}
|
|
|
|
@Override
|
|
public void addChildAt(CSSNode 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.mRight - mNodeRegion.mLeft);
|
|
int height = (int) (mNodeRegion.mBottom - mNodeRegion.mTop);
|
|
|
|
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.mLeft < leftBound) {
|
|
leftBound = region.mLeft;
|
|
overflowsContainer = true;
|
|
}
|
|
|
|
if (region.mRight > rightBound) {
|
|
rightBound = region.mRight;
|
|
overflowsContainer = true;
|
|
}
|
|
|
|
if (region.mTop < topBound) {
|
|
topBound = region.mTop;
|
|
overflowsContainer = true;
|
|
}
|
|
|
|
if (region.mBottom > bottomBound) {
|
|
bottomBound = region.mBottom;
|
|
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.mLeft != left || mNodeRegion.mTop != top || mNodeRegion.mRight != right ||
|
|
mNodeRegion.mBottom != bottom || mNodeRegion.mIsVirtual != 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.reactTag == 0) {
|
|
// 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,
|
|
Math.round(left + mLogicalOffset.left),
|
|
Math.round(top + mLogicalOffset.top),
|
|
Math.round(right + mLogicalOffset.right),
|
|
Math.round(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;
|
|
}
|
|
}
|