/**
* 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.Arrays;
import java.util.List;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewManagerRegistry;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.yoga.YogaDirection;
/**
* FlatUIImplementation builds on top of UIImplementation and allows pre-creating everything
* required for drawing (DrawCommands) and touching (NodeRegions) views in background thread
* for faster drawing and interactions.
*/
public class FlatUIImplementation extends UIImplementation {
public static FlatUIImplementation createInstance(
ReactApplicationContext reactContext,
List<ViewManager> viewManagers,
EventDispatcher eventDispatcher,
boolean memoryImprovementEnabled) {
RCTImageViewManager rctImageViewManager = findRCTImageManager(viewManagers);
if (rctImageViewManager != null) {
Object callerContext = rctImageViewManager.getCallerContext();
if (callerContext != null) {
RCTImageView.setCallerContext(callerContext);
}
}
DraweeRequestHelper.setResources(reactContext.getResources());
TypefaceCache.setAssetManager(reactContext.getAssets());
ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers);
FlatNativeViewHierarchyManager nativeViewHierarchyManager = new FlatNativeViewHierarchyManager(
viewManagerRegistry);
FlatUIViewOperationQueue operationsQueue = new FlatUIViewOperationQueue(
reactContext,
nativeViewHierarchyManager);
return new FlatUIImplementation(
reactContext,
rctImageViewManager,
viewManagerRegistry,
operationsQueue,
eventDispatcher,
memoryImprovementEnabled
);
}
/**
* Helper class that sorts moveTo/moveFrom arrays passed to #manageChildren().
* Not used outside of the said method.
*/
private final MoveProxy mMoveProxy = new MoveProxy();
private final ReactApplicationContext mReactContext;
private @Nullable RCTImageViewManager mRCTImageViewManager;
private final StateBuilder mStateBuilder;
private final boolean mMemoryImprovementEnabled;
private FlatUIImplementation(
ReactApplicationContext reactContext,
@Nullable RCTImageViewManager rctImageViewManager,
ViewManagerRegistry viewManagers,
FlatUIViewOperationQueue operationsQueue,
EventDispatcher eventDispatcher,
boolean memoryImprovementEnabled) {
super(reactContext, viewManagers, operationsQueue, eventDispatcher);
mReactContext = reactContext;
mRCTImageViewManager = rctImageViewManager;
mStateBuilder = new StateBuilder(operationsQueue);
mMemoryImprovementEnabled = memoryImprovementEnabled;
}
@Override
protected ReactShadowNode createRootShadowNode() {
if (mRCTImageViewManager != null) {
// This is not the best place to initialize DraweeRequestHelper, but order of module
// initialization is undefined, and this is pretty much the earliest when we are guarantied
// that Fresco is initalized and DraweeControllerBuilder can be queried. This also happens
// relatively rarely to have any performance considerations.
DraweeRequestHelper.setDraweeControllerBuilder(
mRCTImageViewManager.getDraweeControllerBuilder());
mRCTImageViewManager = null;
}
ReactShadowNode node = new FlatRootShadowNode();
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
if (sharedI18nUtilInstance.isRTL(mReactContext)) {
node.setLayoutDirection(YogaDirection.RTL);
}
return node;
}
@Override
protected ReactShadowNode createShadowNode(String className) {
ReactShadowNode cssNode = super.createShadowNode(className);
if (cssNode instanceof FlatShadowNode || cssNode.isVirtual()) {
return cssNode;
}
ViewManager viewManager = resolveViewManager(className);
return new NativeViewWrapper(viewManager);
}
@Override
protected void handleCreateView(
ReactShadowNode cssNode,
int rootViewTag,
@Nullable ReactStylesDiffMap styles) {
if (cssNode instanceof FlatShadowNode) {
FlatShadowNode node = (FlatShadowNode) cssNode;
if (styles != null) {
node.handleUpdateProperties(styles);
}
if (node.mountsToView()) {
mStateBuilder.enqueueCreateOrUpdateView(node, styles);
}
} else {
super.handleCreateView(cssNode, rootViewTag, styles);
}
}
@Override
protected void handleUpdateView(
ReactShadowNode cssNode,
String className,
ReactStylesDiffMap styles) {
if (cssNode instanceof FlatShadowNode) {
FlatShadowNode node = (FlatShadowNode) cssNode;
node.handleUpdateProperties(styles);
if (node.mountsToView()) {
mStateBuilder.enqueueCreateOrUpdateView(node, styles);
}
} else {
super.handleUpdateView(cssNode, className, styles);
}
}
@Override
public void manageChildren(
int viewTag,
@Nullable ReadableArray moveFrom,
@Nullable ReadableArray moveTo,
@Nullable ReadableArray addChildTags,
@Nullable ReadableArray addAtIndices,
@Nullable ReadableArray removeFrom) {
ReactShadowNode parentNode = resolveShadowNode(viewTag);
// moveFrom and removeFrom are defined in original order before any mutations.
removeChildren(parentNode, moveFrom, moveTo, removeFrom);
// moveTo and addAtIndices are defined in final order after all the mutations applied.
addChildren(parentNode, addChildTags, addAtIndices);
}
@Override
public void setChildren(
int viewTag,
ReadableArray children) {
ReactShadowNode parentNode = resolveShadowNode(viewTag);
for (int i = 0; i < children.size(); i++) {
ReactShadowNode addToChild = resolveShadowNode(children.getInt(i));
addChildAt(parentNode, addToChild, i, i - 1);
}
}
@Override
public void measure(int reactTag, Callback callback) {
measureHelper(reactTag, false, callback);
}
private void measureHelper(int reactTag, boolean relativeToWindow, Callback callback) {
FlatShadowNode node = (FlatShadowNode) resolveShadowNode(reactTag);
if (node.mountsToView()) {
mStateBuilder.ensureBackingViewIsCreated(node);
if (relativeToWindow) {
super.measureInWindow(reactTag, callback);
} else {
super.measure(reactTag, callback);
}
return;
}
// virtual nodes do not have values for width and height, so get these values
// from the first non-virtual parent node
while (node != null && node.isVirtual()) {
node = (FlatShadowNode) node.getParent();
}
if (node == null) {
// everything is virtual, this shouldn't happen so just silently return
return;
}
float width = node.getLayoutWidth();
float height = node.getLayoutHeight();
boolean nodeMountsToView = node.mountsToView();
// this is to avoid double-counting xInParent and yInParent when we visit
// the while loop, below.
float xInParent = nodeMountsToView ? node.getLayoutX() : 0;
float yInParent = nodeMountsToView ? node.getLayoutY() : 0;
while (!node.mountsToView()) {
if (!node.isVirtual()) {
xInParent += node.getLayoutX();
yInParent += node.getLayoutY();
}
node = Assertions.assumeNotNull((FlatShadowNode) node.getParent());
}
float parentWidth = node.getLayoutWidth();
float parentHeight = node.getLayoutHeight();
FlatUIViewOperationQueue operationsQueue = mStateBuilder.getOperationsQueue();
operationsQueue.enqueueMeasureVirtualView(
node.getReactTag(),
xInParent / parentWidth,
yInParent / parentHeight,
width / parentWidth,
height / parentHeight,
relativeToWindow,
callback);
}
private void ensureMountsToViewAndBackingViewIsCreated(int reactTag) {
FlatShadowNode node = (FlatShadowNode) resolveShadowNode(reactTag);
if (node.isBackingViewCreated()) {
return;
}
node.forceMountToView();
mStateBuilder.ensureBackingViewIsCreated(node);
}
@Override
public void findSubviewIn(int reactTag, float targetX, float targetY, Callback callback) {
ensureMountsToViewAndBackingViewIsCreated(reactTag);
super.findSubviewIn(reactTag, targetX, targetY, callback);
}
@Override
public void measureInWindow(int reactTag, Callback callback) {
measureHelper(reactTag, true, callback);
}
@Override
public void addAnimation(int reactTag, int animationID, Callback onSuccess) {
ensureMountsToViewAndBackingViewIsCreated(reactTag);
super.addAnimation(reactTag, animationID, onSuccess);
}
@Override
public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
// Make sure that our target view is actually a view, then delay command dispatch until after
// we have updated the view hierarchy.
ensureMountsToViewAndBackingViewIsCreated(reactTag);
mStateBuilder.enqueueViewManagerCommand(reactTag, commandId, commandArgs);
}
@Override
public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
ensureMountsToViewAndBackingViewIsCreated(reactTag);
super.showPopupMenu(reactTag, items, error, success);
}
@Override
public void sendAccessibilityEvent(int reactTag, int eventType) {
ensureMountsToViewAndBackingViewIsCreated(reactTag);
super.sendAccessibilityEvent(reactTag, eventType);
}
/**
* Removes all children defined by moveFrom and removeFrom from a given parent,
* preparing elements in moveFrom to be re-added at proper index.
*/
private void removeChildren(
ReactShadowNode parentNode,
@Nullable ReadableArray moveFrom,
@Nullable ReadableArray moveTo,
@Nullable ReadableArray removeFrom) {
int prevIndex = Integer.MAX_VALUE;
mMoveProxy.setup(moveFrom, moveTo);
int moveFromIndex = mMoveProxy.size() - 1;
int moveFromChildIndex = (moveFromIndex == -1) ? -1 : mMoveProxy.getMoveFrom(moveFromIndex);
int numToRemove = removeFrom == null ? 0 : removeFrom.size();
int[] indicesToRemove = new int[numToRemove];
if (numToRemove > 0) {
Assertions.assertNotNull(removeFrom);
for (int i = 0; i < numToRemove; i++) {
int indexToRemove = removeFrom.getInt(i);
indicesToRemove[i] = indexToRemove;
}
}
// this isn't guaranteed to be sorted actually
Arrays.sort(indicesToRemove);
int removeFromIndex;
int removeFromChildIndex;
if (removeFrom == null) {
removeFromIndex = -1;
removeFromChildIndex = -1;
} else {
removeFromIndex = indicesToRemove.length - 1;
removeFromChildIndex = indicesToRemove[removeFromIndex];
}
// both moveFrom and removeFrom are already sorted, but combined order is not sorted. Use
// a merge step from mergesort to walk over both arrays and extract elements in sorted order.
while (true) {
if (moveFromChildIndex > removeFromChildIndex) {
moveChild(removeChildAt(parentNode, moveFromChildIndex, prevIndex), moveFromIndex);
prevIndex = moveFromChildIndex;
--moveFromIndex;
moveFromChildIndex = (moveFromIndex == -1) ? -1 : mMoveProxy.getMoveFrom(moveFromIndex);
} else if (removeFromChildIndex > moveFromChildIndex) {
removeChild(removeChildAt(parentNode, removeFromChildIndex, prevIndex), parentNode);
prevIndex = removeFromChildIndex;
--removeFromIndex;
removeFromChildIndex = (removeFromIndex == -1) ? -1 : indicesToRemove[removeFromIndex];
} else {
// moveFromChildIndex == removeFromChildIndex can only be if both are equal to -1
// which means that we exhausted both arrays, and all children are removed.
break;
}
}
}
/**
* Unregisters given element and all of its children from ShadowNodeRegistry,
* and drops all Views used by it and its children.
*/
private void removeChild(ReactShadowNode child, ReactShadowNode parentNode) {
dropNativeViews(child, parentNode);
removeShadowNode(child);
}
private void dropNativeViews(ReactShadowNode child, ReactShadowNode parentNode) {
if (child instanceof FlatShadowNode) {
FlatShadowNode node = (FlatShadowNode) child;
if (node.mountsToView() && node.isBackingViewCreated()) {
int tag = -1;
// this tag is used to remove the reference to this dropping view if it it's clipped.
// we need to figure out the correct "view parent" tag to do this. note that this is
// not necessarily getParent().getReactTag(), since getParent() may represent something
// that's not a View - we need to find the first View (what would represent
// view.getParent() on the ui thread), which is what this code is finding.
ReactShadowNode tmpNode = parentNode;
while (tmpNode != null) {
if (tmpNode instanceof FlatShadowNode) {
FlatShadowNode flatTmpNode = (FlatShadowNode) tmpNode;
if (flatTmpNode.mountsToView() && flatTmpNode.isBackingViewCreated() &&
flatTmpNode.getParent() != null) {
tag = flatTmpNode.getReactTag();
break;
}
}
tmpNode = tmpNode.getParent();
}
// this will recursively drop all subviews
mStateBuilder.dropView(node, tag);
return;
}
}
for (int i = 0, childCount = child.getChildCount(); i != childCount; ++i) {
dropNativeViews(child.getChildAt(i), child);
}
}
/**
* Prepares a given element to be moved to a new position.
*/
private void moveChild(ReactShadowNode child, int moveFromIndex) {
mMoveProxy.setChildMoveFrom(moveFromIndex, child);
}
/**
* Adds all children from addChildTags and moveFrom/moveTo.
*/
private void addChildren(
ReactShadowNode parentNode,
@Nullable ReadableArray addChildTags,
@Nullable ReadableArray addAtIndices) {
int prevIndex = -1;
int moveToIndex;
int moveToChildIndex;
if (mMoveProxy.size() == 0) {
moveToIndex = Integer.MAX_VALUE;
moveToChildIndex = Integer.MAX_VALUE;
} else {
moveToIndex = 0;
moveToChildIndex = mMoveProxy.getMoveTo(0);
}
int numNodesToAdd;
int addToIndex;
int addToChildIndex;
if (addAtIndices == null) {
numNodesToAdd = 0;
addToIndex = Integer.MAX_VALUE;
addToChildIndex = Integer.MAX_VALUE;
} else {
numNodesToAdd = addAtIndices.size();
addToIndex = 0;
addToChildIndex = addAtIndices.getInt(0);
}
// both mMoveProxy and addChildTags are already sorted, but combined order is not sorted. Use
// a merge step from mergesort to walk over both arrays and extract elements in sorted order.
while (true) {
if (addToChildIndex < moveToChildIndex) {
ReactShadowNode addToChild = resolveShadowNode(addChildTags.getInt(addToIndex));
addChildAt(parentNode, addToChild, addToChildIndex, prevIndex);
prevIndex = addToChildIndex;
++addToIndex;
if (addToIndex == numNodesToAdd) {
addToChildIndex = Integer.MAX_VALUE;
} else {
addToChildIndex = addAtIndices.getInt(addToIndex);
}
} else if (moveToChildIndex < addToChildIndex) {
ReactShadowNode moveToChild = mMoveProxy.getChildMoveTo(moveToIndex);
addChildAt(parentNode, moveToChild, moveToChildIndex, prevIndex);
prevIndex = moveToChildIndex;
++moveToIndex;
if (moveToIndex == mMoveProxy.size()) {
moveToChildIndex = Integer.MAX_VALUE;
} else {
moveToChildIndex = mMoveProxy.getMoveTo(moveToIndex);
}
} else {
// moveToChildIndex == addToChildIndex can only be if both are equal to Integer.MAX_VALUE
// which means that we exhausted both arrays, and all children are added.
break;
}
}
}
/**
* Removes a child from parent, verifying that we are removing in descending order.
*/
private static ReactShadowNode removeChildAt(
ReactShadowNode parentNode,
int index,
int prevIndex) {
if (index >= prevIndex) {
throw new RuntimeException(
"Invariant failure, needs sorting! " + index + " >= " + prevIndex);
}
return parentNode.removeChildAt(index);
}
/**
* Adds a child to parent, verifying that we are adding in ascending order.
*/
private static void addChildAt(
ReactShadowNode parentNode,
ReactShadowNode childNode,
int index,
int prevIndex) {
if (index <= prevIndex) {
throw new RuntimeException(
"Invariant failure, needs sorting! " + index + " <= " + prevIndex);
}
parentNode.addChildAt(childNode, index);
}
@Override
protected void updateViewHierarchy() {
super.updateViewHierarchy();
mStateBuilder.afterUpdateViewHierarchy(mEventDispatcher);
}
@Override
protected void applyUpdatesRecursive(
ReactShadowNode cssNode,
float absoluteX,
float absoluteY) {
mStateBuilder.applyUpdates((FlatRootShadowNode) cssNode);
}
@Override
public void removeRootView(int rootViewTag) {
if (mMemoryImprovementEnabled) {
removeRootShadowNode(rootViewTag);
}
mStateBuilder.removeRootView(rootViewTag);
}
@Override
public void setJSResponder(int possiblyVirtualReactTag, boolean blockNativeResponder) {
ReactShadowNode node = resolveShadowNode(possiblyVirtualReactTag);
while (node.isVirtual()) {
node = node.getParent();
}
int tag = node.getReactTag();
// if the node in question doesn't mount to a View, find the first parent that does mount to
// a View. without this, we'll crash when we try to set the JSResponder, since part of that
// is to find the parent view and ask it to not intercept touch events.
while (node instanceof FlatShadowNode && !((FlatShadowNode) node).mountsToView()) {
node = node.getParent();
}
FlatUIViewOperationQueue operationsQueue = mStateBuilder.getOperationsQueue();
operationsQueue.enqueueSetJSResponder(
node == null ? tag : node.getReactTag(),
possiblyVirtualReactTag,
blockNativeResponder);
}
private static @Nullable RCTImageViewManager findRCTImageManager(List<ViewManager> viewManagers) {
for (int i = 0, size = viewManagers.size(); i != size; ++i) {
if (viewManagers.get(i) instanceof RCTImageViewManager) {
return (RCTImageViewManager) viewManagers.get(i);
}
}
return null;
}
}