Files
react-native/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIImplementation.java
Ahmed El-Helw f038ef4e0d Don't resolve root parent when dropping a view in Nodes
Summary:
In Nodes, we added logic when we dropped a view to also pass the
parent, so we could tell the parent that \"hey, this view is now dropped\"
so that it can be released. While this is fine, there are some crashes due
to the fact that the root node is not being found when we drop the child.

I've spent a lot of time thinking about how this could happen, and the only
plausible explanation I can come up with is that we first detach all views
from the root, then drop the root, and then drop one of the children that
was detached. I can't think of a good way why this would happen.

In the interest of protecting from this crash, this patch adds a check as to
whether or not the id of the parent is that of a root id, and, if so, it
doesn't run the logic that tells this view that this view was removed.

This should be safe, because the root most view should not have clipping
enabled (since it's a vanilla view group as opposed to a scrolling view).

Reviewed By: sriramramani

Differential Revision: D3991682
2016-12-19 13:40:34 -08:00

559 lines
19 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.facebook.csslayout.CSSDirection;
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.react.views.image.ReactImageManager;
/**
* 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) {
ReactImageManager reactImageManager = findReactImageManager(viewManagers);
if (reactImageManager != null) {
Object callerContext = reactImageManager.getCallerContext();
if (callerContext != null) {
RCTImageView.setCallerContext(callerContext);
}
}
DraweeRequestHelper.setResources(reactContext.getResources());
TypefaceCache.setAssetManager(reactContext.getAssets());
viewManagers = new ArrayList<>(viewManagers);
viewManagers.add(new RCTViewManager());
viewManagers.add(new RCTTextManager());
viewManagers.add(new RCTRawTextManager());
viewManagers.add(new RCTVirtualTextManager());
viewManagers.add(new RCTTextInlineImageManager());
viewManagers.add(new RCTImageViewManager());
viewManagers.add(new RCTTextInputManager());
viewManagers.add(new RCTViewPagerManager());
viewManagers.add(new FlatARTSurfaceViewManager());
viewManagers.add(new RCTModalHostManager());
ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers);
FlatNativeViewHierarchyManager nativeViewHierarchyManager = new FlatNativeViewHierarchyManager(
viewManagerRegistry);
FlatUIViewOperationQueue operationsQueue = new FlatUIViewOperationQueue(
reactContext,
nativeViewHierarchyManager);
return new FlatUIImplementation(
reactContext,
reactImageManager,
viewManagerRegistry,
operationsQueue,
eventDispatcher
);
}
/**
* 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 StateBuilder mStateBuilder;
private @Nullable ReactImageManager mReactImageManager;
private final ReactApplicationContext mReactContext;
private FlatUIImplementation(
ReactApplicationContext reactContext,
@Nullable ReactImageManager reactImageManager,
ViewManagerRegistry viewManagers,
FlatUIViewOperationQueue operationsQueue,
EventDispatcher eventDispatcher) {
super(reactContext, viewManagers, operationsQueue, eventDispatcher);
mReactContext = reactContext;
mStateBuilder = new StateBuilder(operationsQueue);
mReactImageManager = reactImageManager;
}
@Override
protected ReactShadowNode createRootShadowNode() {
if (mReactImageManager != 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(
mReactImageManager.getDraweeControllerBuilder());
mReactImageManager = null;
}
ReactShadowNode node = new FlatRootShadowNode();
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
if (sharedI18nUtilInstance.isRTL(mReactContext)) {
node.setDirection(CSSDirection.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;
}
float width = node.getLayoutWidth();
float height = node.getLayoutHeight();
float xInParent = node.getLayoutX();
float yInParent = node.getLayoutY();
while (true) {
node = Assertions.assumeNotNull((FlatShadowNode) node.getParent());
if (node.mountsToView()) {
mStateBuilder.ensureBackingViewIsCreated(node);
break;
}
xInParent += node.getLayoutX();
yInParent += node.getLayoutY();
}
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) {
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);
removeShadowNode(node);
return;
}
}
for (int i = 0, childCount = child.getChildCount(); i != childCount; ++i) {
removeChild(child.getChildAt(i), child);
}
removeShadowNode(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) {
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 ReactImageManager findReactImageManager(List<ViewManager> viewManagers) {
for (int i = 0, size = viewManagers.size(); i != size; ++i) {
if (viewManagers.get(i) instanceof ReactImageManager) {
return (ReactImageManager) viewManagers.get(i);
}
}
return null;
}
}