/**
* 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 java.util.ArrayList;
import android.util.SparseIntArray;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.OnLayoutEvent;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.events.EventDispatcher;
/**
* Shadow node hierarchy by itself cannot display UI, it is only a representation of what UI should
* be from JavaScript perspective. StateBuilder is a helper class that walks the shadow node tree
* and collects information into an operation queue that is run on the UI thread and applied to the
* non-shadow hierarchy of Views that Android can finally display.
*/
/* package */ final class StateBuilder {
/* package */ static final float[] EMPTY_FLOAT_ARRAY = new float[0];
/* package */ static final SparseIntArray EMPTY_SPARSE_INT = new SparseIntArray();
private static final boolean SKIP_UP_TO_DATE_NODES = true;
// Optimization to avoid re-allocating zero length arrays.
private static final int[] EMPTY_INT_ARRAY = new int[0];
private final FlatUIViewOperationQueue mOperationsQueue;
private final ElementsList<DrawCommand> mDrawCommands =
new ElementsList<>(DrawCommand.EMPTY_ARRAY);
private final ElementsList<AttachDetachListener> mAttachDetachListeners =
new ElementsList<>(AttachDetachListener.EMPTY_ARRAY);
private final ElementsList<NodeRegion> mNodeRegions =
new ElementsList<>(NodeRegion.EMPTY_ARRAY);
private final ElementsList<FlatShadowNode> mNativeChildren =
new ElementsList<>(FlatShadowNode.EMPTY_ARRAY);
private final ArrayList<FlatShadowNode> mViewsToDetachAllChildrenFrom = new ArrayList<>();
private final ArrayList<FlatShadowNode> mViewsToDetach = new ArrayList<>();
private final ArrayList<Integer> mViewsToDrop = new ArrayList<>();
private final ArrayList<Integer> mParentsForViewsToDrop = new ArrayList<>();
private final ArrayList<OnLayoutEvent> mOnLayoutEvents = new ArrayList<>();
private final ArrayList<UIViewOperationQueue.UIOperation> mUpdateViewBoundsOperations =
new ArrayList<>();
private final ArrayList<UIViewOperationQueue.UIOperation> mViewManagerCommands =
new ArrayList<>();
private @Nullable FlatUIViewOperationQueue.DetachAllChildrenFromViews mDetachAllChildrenFromViews;
/* package */ StateBuilder(FlatUIViewOperationQueue operationsQueue) {
mOperationsQueue = operationsQueue;
}
/* package */ FlatUIViewOperationQueue getOperationsQueue() {
return mOperationsQueue;
}
/**
* Given a root of the laid-out shadow node hierarchy, walks the tree and generates arrays from
* element lists that are mounted in the UI thread to FlatViewGroups to handle drawing, touch,
* and other logic.
*/
/* package */ void applyUpdates(FlatShadowNode node) {
float width = node.getLayoutWidth();
float height = node.getLayoutHeight();
float left = node.getLayoutX();
float top = node.getLayoutY();
float right = left + width;
float bottom = top + height;
collectStateForMountableNode(
node,
left,
top,
right,
bottom,
Float.NEGATIVE_INFINITY,
Float.NEGATIVE_INFINITY,
Float.POSITIVE_INFINITY,
Float.POSITIVE_INFINITY);
updateViewBounds(node, left, top, right, bottom);
}
/**
* Run after the shadow node hierarchy is updated. Detaches all children from Views that are
* changing their native children, updates views, and dispatches commands before discarding any
* dropped views.
*
* @param eventDispatcher Dispatcher for onLayout events.
*/
void afterUpdateViewHierarchy(EventDispatcher eventDispatcher) {
if (mDetachAllChildrenFromViews != null) {
int[] viewsToDetachAllChildrenFrom = collectViewTags(mViewsToDetachAllChildrenFrom);
mViewsToDetachAllChildrenFrom.clear();
mDetachAllChildrenFromViews.setViewsToDetachAllChildrenFrom(viewsToDetachAllChildrenFrom);
mDetachAllChildrenFromViews = null;
}
for (int i = 0, size = mUpdateViewBoundsOperations.size(); i != size; ++i) {
mOperationsQueue.enqueueFlatUIOperation(mUpdateViewBoundsOperations.get(i));
}
mUpdateViewBoundsOperations.clear();
// Process view manager commands after bounds operations, so that any UI operations have already
// happened before we actually dispatch the view manager command. This prevents things like
// commands going to empty parents and views not yet being created.
for (int i = 0, size = mViewManagerCommands.size(); i != size; i++) {
mOperationsQueue.enqueueFlatUIOperation(mViewManagerCommands.get(i));
}
mViewManagerCommands.clear();
// This could be more efficient if EventDispatcher had a batch mode
// to avoid multiple synchronized calls.
for (int i = 0, size = mOnLayoutEvents.size(); i != size; ++i) {
eventDispatcher.dispatchEvent(mOnLayoutEvents.get(i));
}
mOnLayoutEvents.clear();
if (mViewsToDrop.size() > 0) {
mOperationsQueue.enqueueDropViews(mViewsToDrop, mParentsForViewsToDrop);
mViewsToDrop.clear();
mParentsForViewsToDrop.clear();
}
mOperationsQueue.enqueueProcessLayoutRequests();
}
/* package */ void removeRootView(int rootViewTag) {
// Note root view tags with a negative value.
mViewsToDrop.add(-rootViewTag);
mParentsForViewsToDrop.add(-1);
}
/**
* Adds a draw command to the element list for the current scope. Allows collectState within the
* shadow node to add commands.
*
* @param drawCommand The draw command to add.
*/
/* package */ void addDrawCommand(AbstractDrawCommand drawCommand) {
mDrawCommands.add(drawCommand);
}
/**
* Adds a listener to the element list for the current scope. Allows collectState within the
* shadow node to add listeners.
*
* @param listener The listener to add
*/
/* package */ void addAttachDetachListener(AttachDetachListener listener) {
mAttachDetachListeners.add(listener);
}
/**
* Adds a command for a view manager to the queue. We have to delay adding it to the operations
* queue until we have added our view moves, creations and updates.
*
* @param reactTag The react tag of the command target.
* @param commandId ID of the command.
* @param commandArgs Arguments for the command.
*/
/* package */ void enqueueViewManagerCommand(
int reactTag,
int commandId,
ReadableArray commandArgs) {
mViewManagerCommands.add(
mOperationsQueue.createViewManagerCommand(reactTag, commandId, commandArgs));
}
/**
* Create a backing view for a node, or update the backing view if it has already been created.
*
* @param node The node to create the backing view for.
* @param styles Styles for the view.
*/
/* package */ void enqueueCreateOrUpdateView(
FlatShadowNode node,
@Nullable ReactStylesDiffMap styles) {
if (node.isBackingViewCreated()) {
// If the View is already created, make sure to propagate the new styles.
mOperationsQueue.enqueueUpdateProperties(
node.getReactTag(),
node.getViewClass(),
styles);
} else {
mOperationsQueue.enqueueCreateView(
node.getThemedContext(),
node.getReactTag(),
node.getViewClass(),
styles);
node.signalBackingViewIsCreated();
}
}
/**
* Create a backing view for a node if not already created.
*
* @param node The node to create the backing view for.
*/
/* package */ void ensureBackingViewIsCreated(FlatShadowNode node) {
if (node.isBackingViewCreated()) {
return;
}
int tag = node.getReactTag();
mOperationsQueue.enqueueCreateView(node.getThemedContext(), tag, node.getViewClass(), null);
node.signalBackingViewIsCreated();
}
/**
* Enqueue dropping of the view for a node that has a backing view. Used in conjuction with
* remove the node from the shadow hierarchy.
*
* @param node The node to drop the backing view for.
*/
/* package */ void dropView(FlatShadowNode node, int parentReactTag) {
mViewsToDrop.add(node.getReactTag());
mParentsForViewsToDrop.add(parentReactTag);
}
/**
* Adds a node region to the element list for the current scope. Allows collectState to add
* regions.
*
* @param node The node to add a region for.
* @param left Bound of the region.
* @param top Bound of the region.
* @param right Bound of the region.
* @param bottom Bound of the region.
* @param isVirtual True if the region does not map to a native view. Used to determine touch
* targets.
*/
private void addNodeRegion(
FlatShadowNode node,
float left,
float top,
float right,
float bottom,
boolean isVirtual) {
if (left == right || top == bottom) {
// no point in adding an empty NodeRegion
return;
}
node.updateNodeRegion(left, top, right, bottom, isVirtual);
if (node.doesDraw()) {
mNodeRegions.add(node.getNodeRegion());
}
}
/**
* Adds a native child to the element list for the current scope. Allows collectState to add
* native children.
*
* @param nativeChild The view-backed native child to add.
*/
private void addNativeChild(FlatShadowNode nativeChild) {
mNativeChildren.add(nativeChild);
}
/**
* Updates boundaries of a View that a give nodes maps to.
*/
private void updateViewBounds(
FlatShadowNode node,
float left,
float top,
float right,
float bottom) {
int viewLeft = Math.round(left);
int viewTop = Math.round(top);
int viewRight = Math.round(right);
int viewBottom = Math.round(bottom);
if (node.getViewLeft() == viewLeft && node.getViewTop() == viewTop &&
node.getViewRight() == viewRight && node.getViewBottom() == viewBottom) {
// nothing changed.
return;
}
// this will optionally measure and layout the View this node maps to.
node.setViewBounds(viewLeft, viewTop, viewRight, viewBottom);
int tag = node.getReactTag();
mUpdateViewBoundsOperations.add(
mOperationsQueue.createUpdateViewBounds(tag, viewLeft, viewTop, viewRight, viewBottom));
}
/**
* Collects state (Draw commands, listeners, regions, native children) for a given node that will
* mount to a View. Returns true if this node or any of its descendants that mount to View
* generated any updates.
*/
private boolean collectStateForMountableNode(
FlatShadowNode node,
float left,
float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
boolean hasUpdates = node.hasNewLayout();
boolean expectingUpdate = hasUpdates || node.isUpdated() || node.hasUnseenUpdates() ||
node.clipBoundsChanged(clipLeft, clipTop, clipRight, clipBottom);
if (SKIP_UP_TO_DATE_NODES && !expectingUpdate) {
return false;
}
node.setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
mDrawCommands.start(node.getDrawCommands());
mAttachDetachListeners.start(node.getAttachDetachListeners());
mNodeRegions.start(node.getNodeRegions());
mNativeChildren.start(node.getNativeChildren());
boolean isAndroidView = false;
boolean needsCustomLayoutForChildren = false;
if (node instanceof AndroidView) {
AndroidView androidView = (AndroidView) node;
updateViewPadding(androidView, node.getReactTag());
isAndroidView = true;
needsCustomLayoutForChildren = androidView.needsCustomLayoutForChildren();
// AndroidView might scroll (e.g. ScrollView) so we need to reset clip bounds here
// Otherwise, we might scroll clipped content. If AndroidView doesn't scroll, this is still
// harmless, because AndroidView will do its own clipping anyway.
clipLeft = Float.NEGATIVE_INFINITY;
clipTop = Float.NEGATIVE_INFINITY;
clipRight = Float.POSITIVE_INFINITY;
clipBottom = Float.POSITIVE_INFINITY;
}
if (!isAndroidView && node.isVirtualAnchor()) {
// If RCTText is mounted to View, virtual children will not receive any touch events
// because they don't get added to nodeRegions, so nodeRegions will be empty and
// FlatViewGroup.reactTagForTouch() will always return RCTText's id. To fix the issue,
// manually add nodeRegion so it will have exactly one NodeRegion, and virtual nodes will
// be able to receive touch events.
addNodeRegion(node, left, top, right, bottom, true);
}
boolean descendantUpdated = collectStateRecursively(
node,
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom,
isAndroidView,
needsCustomLayoutForChildren);
boolean shouldUpdateMountState = false;
final DrawCommand[] drawCommands = mDrawCommands.finish();
if (drawCommands != null) {
shouldUpdateMountState = true;
node.setDrawCommands(drawCommands);
}
final AttachDetachListener[] listeners = mAttachDetachListeners.finish();
if (listeners != null) {
shouldUpdateMountState = true;
node.setAttachDetachListeners(listeners);
}
final NodeRegion[] nodeRegions = mNodeRegions.finish();
if (nodeRegions != null) {
shouldUpdateMountState = true;
node.setNodeRegions(nodeRegions);
} else if (descendantUpdated) {
// one of the descendant's value for overflows container may have changed, so
// we still need to update ours.
node.updateOverflowsContainer();
}
// We need to finish the native children so that we can process clipping FlatViewGroup.
final FlatShadowNode[] nativeChildren = mNativeChildren.finish();
if (shouldUpdateMountState) {
if (node.clipsSubviews()) {
// Node is a clipping FlatViewGroup, so lets do some calculations off the UI thread.
// DrawCommandManager has a better explanation of the data incoming from these calculations,
// and is where they are actually used.
float[] commandMaxBottom = EMPTY_FLOAT_ARRAY;
float[] commandMinTop = EMPTY_FLOAT_ARRAY;
SparseIntArray drawViewIndexMap = EMPTY_SPARSE_INT;
if (drawCommands != null) {
drawViewIndexMap = new SparseIntArray();
commandMaxBottom = new float[drawCommands.length];
commandMinTop = new float[drawCommands.length];
if (node.isHorizontal()) {
HorizontalDrawCommandManager
.fillMaxMinArrays(drawCommands, commandMaxBottom, commandMinTop, drawViewIndexMap);
} else {
VerticalDrawCommandManager
.fillMaxMinArrays(drawCommands, commandMaxBottom, commandMinTop, drawViewIndexMap);
}
}
float[] regionMaxBottom = EMPTY_FLOAT_ARRAY;
float[] regionMinTop = EMPTY_FLOAT_ARRAY;
if (nodeRegions != null) {
regionMaxBottom = new float[nodeRegions.length];
regionMinTop = new float[nodeRegions.length];
if (node.isHorizontal()) {
HorizontalDrawCommandManager
.fillMaxMinArrays(nodeRegions, regionMaxBottom, regionMinTop);
} else {
VerticalDrawCommandManager
.fillMaxMinArrays(nodeRegions, regionMaxBottom, regionMinTop);
}
}
boolean willMountViews = nativeChildren != null;
mOperationsQueue.enqueueUpdateClippingMountState(
node.getReactTag(),
drawCommands,
drawViewIndexMap,
commandMaxBottom,
commandMinTop,
listeners,
nodeRegions,
regionMaxBottom,
regionMinTop,
willMountViews);
} else {
mOperationsQueue.enqueueUpdateMountState(
node.getReactTag(),
drawCommands,
listeners,
nodeRegions);
}
}
if (node.hasUnseenUpdates()) {
node.onCollectExtraUpdates(mOperationsQueue);
node.markUpdateSeen();
}
if (nativeChildren != null) {
updateNativeChildren(node, node.getNativeChildren(), nativeChildren);
}
boolean updated = shouldUpdateMountState || nativeChildren != null || descendantUpdated;
if (!expectingUpdate && updated) {
throw new RuntimeException("Node " + node.getReactTag() + " updated unexpectedly.");
}
return updated;
}
/**
* Handles updating the children of a node when they change. Updates the shadow node and
* enqueues state updates that will eventually be run on the UI thread.
*
* @param node The node to update native children for.
* @param oldNativeChildren The previously mounted native children.
* @param newNativeChildren The newly mounted native children.
*/
private void updateNativeChildren(
FlatShadowNode node,
FlatShadowNode[] oldNativeChildren,
FlatShadowNode[] newNativeChildren) {
node.setNativeChildren(newNativeChildren);
if (mDetachAllChildrenFromViews == null) {
mDetachAllChildrenFromViews = mOperationsQueue.enqueueDetachAllChildrenFromViews();
}
if (oldNativeChildren.length != 0) {
mViewsToDetachAllChildrenFrom.add(node);
}
int tag = node.getReactTag();
int numViewsToAdd = newNativeChildren.length;
final int[] viewsToAdd;
if (numViewsToAdd == 0) {
viewsToAdd = EMPTY_INT_ARRAY;
} else {
viewsToAdd = new int[numViewsToAdd];
int i = 0;
for (FlatShadowNode child : newNativeChildren) {
if (child.getNativeParentTag() == tag) {
viewsToAdd[i] = -child.getReactTag();
} else {
viewsToAdd[i] = child.getReactTag();
}
// all views we add are first start detached
child.setNativeParentTag(-1);
++i;
}
}
// Populate an array of views to detach.
// These views still have their native parent set as opposed to being reset to -1
for (FlatShadowNode child : oldNativeChildren) {
if (child.getNativeParentTag() == tag) {
// View is attached to old parent and needs to be removed.
mViewsToDetach.add(child);
child.setNativeParentTag(-1);
}
}
final int[] viewsToDetach = collectViewTags(mViewsToDetach);
mViewsToDetach.clear();
// restore correct parent tag
for (FlatShadowNode child : newNativeChildren) {
child.setNativeParentTag(tag);
}
mOperationsQueue.enqueueUpdateViewGroup(tag, viewsToAdd, viewsToDetach);
}
/**
* Recursively walks node tree from a given node and collects draw commands, listeners, node
* regions and native children. Calls collect state on the node, then processNodeAndCollectState
* for the recursion.
*/
private boolean collectStateRecursively(
FlatShadowNode node,
float left,
float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom,
boolean isAndroidView,
boolean needsCustomLayoutForChildren) {
if (node.hasNewLayout()) {
node.markLayoutSeen();
}
float roundedLeft = roundToPixel(left);
float roundedTop = roundToPixel(top);
float roundedRight = roundToPixel(right);
float roundedBottom = roundToPixel(bottom);
// notify JS about layout event if requested
if (node.shouldNotifyOnLayout()) {
OnLayoutEvent layoutEvent = node.obtainLayoutEvent(
Math.round(node.getLayoutX()),
Math.round(node.getLayoutY()),
(int) (roundedRight - roundedLeft),
(int) (roundedBottom - roundedTop));
if (layoutEvent != null) {
mOnLayoutEvents.add(layoutEvent);
}
}
if (node.clipToBounds()) {
clipLeft = Math.max(left, clipLeft);
clipTop = Math.max(top, clipTop);
clipRight = Math.min(right, clipRight);
clipBottom = Math.min(bottom, clipBottom);
}
node.collectState(
this,
roundedLeft,
roundedTop,
roundedRight,
roundedBottom,
roundToPixel(clipLeft),
roundToPixel(clipTop),
roundToPixel(clipRight),
clipBottom);
boolean updated = false;
for (int i = 0, childCount = node.getChildCount(); i != childCount; ++i) {
ReactShadowNode child = node.getChildAt(i);
if (child.isVirtual()) {
continue;
}
updated |= processNodeAndCollectState(
(FlatShadowNode) child,
left,
top,
clipLeft,
clipTop,
clipRight,
clipBottom,
isAndroidView,
needsCustomLayoutForChildren);
}
node.resetUpdated();
return updated;
}
/**
* Collects state and enqueues View boundary updates for a given node tree. Returns true if
* this node or any of its descendants that mount to View generated any updates.
*/
private boolean processNodeAndCollectState(
FlatShadowNode node,
float parentLeft,
float parentTop,
float parentClipLeft,
float parentClipTop,
float parentClipRight,
float parentClipBottom,
boolean parentIsAndroidView,
boolean needsCustomLayout) {
float width = node.getLayoutWidth();
float height = node.getLayoutHeight();
float left = parentLeft + node.getLayoutX();
float top = parentTop + node.getLayoutY();
float right = left + width;
float bottom = top + height;
boolean mountsToView = node.mountsToView();
final boolean updated;
if (!parentIsAndroidView) {
addNodeRegion(node, left, top, right, bottom, !mountsToView);
}
if (mountsToView) {
ensureBackingViewIsCreated(node);
addNativeChild(node);
updated = collectStateForMountableNode(
node,
0, // left - left
0, // top - top
right - left,
bottom - top,
parentClipLeft - left,
parentClipTop - top,
parentClipRight - left,
parentClipBottom - top);
if (!parentIsAndroidView) {
mDrawCommands.add(node.collectDrawView(
left,
top,
right,
bottom,
parentClipLeft,
parentClipTop,
parentClipRight,
parentClipBottom));
}
if (!needsCustomLayout) {
updateViewBounds(node, left, top, right, bottom);
}
} else {
updated = collectStateRecursively(
node,
left,
top,
right,
bottom,
parentClipLeft,
parentClipTop,
parentClipRight,
parentClipBottom,
false,
false);
}
return updated;
}
private void updateViewPadding(AndroidView androidView, int reactTag) {
if (androidView.isPaddingChanged()) {
mOperationsQueue.enqueueSetPadding(
reactTag,
Math.round(androidView.getPadding(Spacing.LEFT)),
Math.round(androidView.getPadding(Spacing.TOP)),
Math.round(androidView.getPadding(Spacing.RIGHT)),
Math.round(androidView.getPadding(Spacing.BOTTOM)));
androidView.resetPaddingChanged();
}
}
private static int[] collectViewTags(ArrayList<FlatShadowNode> views) {
int numViews = views.size();
if (numViews == 0) {
return EMPTY_INT_ARRAY;
}
int[] viewTags = new int[numViews];
for (int i = 0; i < numViews; ++i) {
viewTags[i] = views.get(i).getReactTag();
}
return viewTags;
}
/**
* This is what Math.round() does, except it returns float.
*/
private static float roundToPixel(float pos) {
return (float) Math.floor(pos + 0.5f);
}
}