Gather command and node region information off the UI thread.

Summary: This optimizes node region searches in clipping cases, and does position calculation for drawCommands off of the UI thread.

Reviewed By: ahmedre

Differential Revision: D3665301
This commit is contained in:
Seth Kirby 2016-08-08 16:05:36 -07:00 committed by Ahmed El-Helw
parent ca79e6cf30
commit 192c99a4f6
13 changed files with 598 additions and 105 deletions

View File

@ -19,7 +19,7 @@ import android.graphics.Color;
* The idea is to be able to reuse unmodified objects when we build up DrawCommands before we ship
* them to UI thread, but we can only do that if DrawCommands are immutable.
*/
/* package */ abstract class AbstractDrawCommand implements DrawCommand, Cloneable {
/* package */ abstract class AbstractDrawCommand extends DrawCommand implements Cloneable {
private float mLeft;
private float mTop;

View File

@ -9,12 +9,18 @@
package com.facebook.react.flat;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.View;
import android.view.animation.Animation;
@ -27,38 +33,133 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
*/
/* package */ final class ClippingDrawCommandManager extends DrawCommandManager {
private final FlatViewGroup mFlatViewGroup;
DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
private float[] mCommandMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY;
private float[] mCommandMinTop = StateBuilder.EMPTY_FLOAT_ARRAY;
// lookups in o(1) instead of o(log n) - trade space for time
private final Map<Integer, DrawView> mDrawViewMap = new HashMap<>();
// When grandchildren are promoted, these can only be FlatViewGroups, but we need to handle the
// case that we clip subviews and don't promote grandchildren.
private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY;
private float[] mRegionMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY;
private float[] mRegionMinTop = StateBuilder.EMPTY_FLOAT_ARRAY;
// Onscreen bounds of draw command array.
private int mStart;
private int mStop;
// Mapping of ids to index position within the draw command array. O(log n) lookups should be
// less in our case because of the large constant overhead and auto boxing of the map.
private SparseIntArray mDrawViewIndexMap = StateBuilder.EMPTY_SPARSE_INT;
// Map of views that are currently clipped.
private final Map<Integer, View> mClippedSubviews = new HashMap<>();
private final Rect mClippingRect = new Rect();
// Used in updating the clipping rect, as sometimes we want to detach all views, which means we
// need to temporarily store the views we are detaching and removing. These are always of size
// 0, except when used in update clipping rect.
private final SparseArray<View> mViewsToRemove = new SparseArray<>();
private final ArrayList<View> mViewsToKeep = new ArrayList<>();
ClippingDrawCommandManager(FlatViewGroup flatViewGroup, DrawCommand[] drawCommands) {
mFlatViewGroup = flatViewGroup;
initialSetup(drawCommands);
}
private void initialSetup(DrawCommand[] drawCommands) {
mountDrawCommands(drawCommands);
mountDrawCommands(
drawCommands,
mDrawViewIndexMap,
mCommandMaxBottom,
mCommandMinTop,
true);
updateClippingRect();
}
@Override
public void mountDrawCommands(DrawCommand[] drawCommands) {
public void mountDrawCommands(
DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] maxBottom,
float[] minTop,
boolean willMountViews) {
mDrawCommands = drawCommands;
mDrawViewMap.clear();
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {
DrawView drawView = (DrawView) drawCommand;
mDrawViewMap.put(drawView.reactTag, drawView);
mCommandMaxBottom = maxBottom;
mCommandMinTop = minTop;
mDrawViewIndexMap = drawViewIndexMap;
if (mClippingRect.bottom != mClippingRect.top) {
mStart = Arrays.binarySearch(mCommandMaxBottom, mClippingRect.top);
if (mStart < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
mStart = ~mStart;
}
mStop = Arrays.binarySearch(
mCommandMinTop,
mStart,
mCommandMinTop.length,
mClippingRect.bottom);
if (mStop < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
mStop = ~mStop;
}
if (!willMountViews) {
// If we are not mounting views, we still need to update view indices and positions. It is
// possible that a child changed size and we still need new clipping even though we are not
// mounting views.
updateClippingToCurrentRect();
}
}
}
@Override
public void mountNodeRegions(NodeRegion[] nodeRegions, float[] maxBottom, float[] minTop) {
mNodeRegions = nodeRegions;
mRegionMaxBottom = maxBottom;
mRegionMinTop = minTop;
}
@Override
public @Nullable NodeRegion virtualNodeRegionWithinBounds(float touchX, float touchY) {
int i = Arrays.binarySearch(mRegionMinTop, touchY + 0.0001f);
if (i < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
i = ~i;
}
while (i-- > 0) {
NodeRegion nodeRegion = mNodeRegions[i];
if (!nodeRegion.mIsVirtual) {
// only interested in virtual nodes
continue;
}
if (mRegionMaxBottom[i] < touchY) {
break;
}
if (nodeRegion.withinBounds(touchX, touchY)) {
return nodeRegion;
}
}
return null;
}
@Override
public @Nullable NodeRegion anyNodeRegionWithinBounds(float touchX, float touchY) {
int i = Arrays.binarySearch(mRegionMinTop, touchY + 0.0001f);
if (i < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
i = ~i;
}
while (i-- > 0) {
NodeRegion nodeRegion = mNodeRegions[i];
if (mRegionMaxBottom[i] < touchY) {
break;
}
if (nodeRegion.withinBounds(touchX, touchY)) {
return nodeRegion;
}
}
return null;
}
private void clip(int id, View view) {
mClippedSubviews.put(id, view);
}
@ -78,13 +179,19 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
@Override
public void mountViews(ViewResolver viewResolver, int[] viewsToAdd, int[] viewsToDetach) {
for (int viewToAdd : viewsToAdd) {
if (viewToAdd > 0) {
// Views that are just temporarily detached are marked with a negative value.
boolean newView = viewToAdd > 0;
if (!newView) {
viewToAdd = -viewToAdd;
}
int commandArrayIndex = mDrawViewIndexMap.get(viewToAdd);
DrawView drawView = (DrawView) mDrawCommands[commandArrayIndex];
View view = viewResolver.getView(drawView.reactTag);
ensureViewHasNoParent(view);
if (newView) {
// This view was not previously attached to this parent.
View view = viewResolver.getView(viewToAdd);
ensureViewHasNoParent(view);
DrawView drawView = Assertions.assertNotNull(mDrawViewMap.get(viewToAdd));
drawView.mWasMounted = true;
if (animating(view) || withinBounds(drawView)) {
if (animating(view) || withinBounds(commandArrayIndex)) {
// View should be drawn. This view can't currently be clipped because it wasn't
// previously attached to this parent.
mFlatViewGroup.addViewInLayout(view);
@ -93,9 +200,6 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
}
} else {
// This view was previously attached, and just temporarily detached.
DrawView drawView = Assertions.assertNotNull(mDrawViewMap.get(-viewToAdd));
View view = viewResolver.getView(drawView.reactTag);
ensureViewHasNoParent(view);
if (drawView.mWasMounted) {
// The DrawView has been mounted before.
if (isNotClipped(drawView.reactTag)) {
@ -109,7 +213,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
// The DrawView has not been mounted before, which means the bounds changed and triggered
// a new DrawView when it was collected from the shadow node. We have a view with the
// same id temporarily detached, but its bounds have changed.
if (animating(view) || withinBounds(drawView)) {
if (animating(view) || withinBounds(commandArrayIndex)) {
// View should be drawn.
if (isClipped(drawView.reactTag)) {
// View was clipped, so add it.
@ -151,13 +255,9 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
return animation != null && !animation.hasEnded();
}
// Return true if a DrawView is currently onscreen.
boolean withinBounds(DrawView drawView) {
return mClippingRect.intersects(
drawView.mLogicalLeft,
drawView.mLogicalTop,
drawView.mLogicalRight,
drawView.mLogicalBottom);
// Return true if a command index is currently onscreen.
boolean withinBounds(int i) {
return mStart <= i && i < mStop;
}
@Override
@ -169,35 +269,102 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
return false;
}
int index = 0;
boolean needsInvalidate = false;
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {
DrawView drawView = (DrawView) drawCommand;
View view = mClippedSubviews.get(drawView.reactTag);
if (view == null) {
// Not clipped, visible
view = mFlatViewGroup.getChildAt(index++);
if (!animating(view) && !withinBounds(drawView)) {
// Now off the screen. Don't invalidate in this case, as the canvas should not be
// redrawn unless new elements are coming onscreen.
clip(drawView.reactTag, view);
mFlatViewGroup.removeViewsInLayout(--index, 1);
}
} else {
// Clipped, invisible. We obviously aren't animating here, as if we were then we would not
// have clipped in the first place.
if (withinBounds(drawView)) {
// Now on the screen. Invalidate as we have a new element to draw.
unclip(drawView.reactTag);
mFlatViewGroup.addViewInLayout(view, index++);
needsInvalidate = true;
}
}
int start = Arrays.binarySearch(mCommandMaxBottom, mClippingRect.top);
if (start < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
start = ~start;
}
int stop = Arrays.binarySearch(
mCommandMinTop,
start,
mCommandMinTop.length,
mClippingRect.bottom);
if (stop < 0) {
// We don't care whether we matched or not, but positive indices are helpful.
stop = ~stop;
}
if (mStart <= start && stop <= mStop) {
return false;
}
mStart = start;
mStop = stop;
updateClippingToCurrentRect();
return true;
}
private void updateClippingToCurrentRect() {
for (int i = 0, size = mFlatViewGroup.getChildCount(); i < size; i++) {
View view = mFlatViewGroup.getChildAt(i);
int index = mDrawViewIndexMap.get(view.getId());
if (withinBounds(index) || animating(view)) {
mViewsToKeep.add(view);
} else {
mViewsToRemove.append(i, view);
clip(view.getId(), view);
}
}
return needsInvalidate;
int removeSize = mViewsToRemove.size();
boolean removeAll = removeSize > 2;
if (removeAll) {
// Detach all, as we are changing quite a few views, whether flinging or otherwise.
mFlatViewGroup.detachAllViewsFromParent();
for (int i = 0; i < removeSize; i++) {
mFlatViewGroup.removeDetachedView(mViewsToRemove.valueAt(i));
}
} else {
// Simple clipping sweep, as we are changing relatively few views.
while (removeSize-- > 0) {
mFlatViewGroup.removeViewsInLayout(mViewsToRemove.keyAt(removeSize), 1);
}
}
mViewsToRemove.clear();
int current = mStart;
int childIndex = 0;
for (int i = 0, size = mViewsToKeep.size(); i < size; i++) {
View view = mViewsToKeep.get(i);
int commandIndex = mDrawViewIndexMap.get(view.getId());
if (current <= commandIndex) {
while (current != commandIndex) {
if (mDrawCommands[current] instanceof DrawView) {
DrawView drawView = (DrawView) mDrawCommands[current];
mFlatViewGroup.addViewInLayout(
Assertions.assumeNotNull(mClippedSubviews.get(drawView.reactTag)),
childIndex++);
unclip(drawView.reactTag);
}
current++;
}
// We are currently at the command index, but we want to increment beyond it.
current++;
}
if (removeAll) {
mFlatViewGroup.attachViewToParent(view, childIndex);
}
// We want to make sure we increment the child index even if we didn't detach it to maintain
// order.
childIndex++;
}
mViewsToKeep.clear();
while (current < mStop) {
if (mDrawCommands[current] instanceof DrawView) {
DrawView drawView = (DrawView) mDrawCommands[current];
mFlatViewGroup.addViewInLayout(
Assertions.assumeNotNull(mClippedSubviews.get(drawView.reactTag)),
childIndex++);
unclip(drawView.reactTag);
}
current++;
}
}
@Override
@ -212,20 +379,42 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
@Override
public void draw(Canvas canvas) {
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {
if (isNotClipped(((DrawView) drawCommand).reactTag)) {
drawCommand.draw(mFlatViewGroup, canvas);
int commandIndex = mStart;
int size = mFlatViewGroup.getChildCount();
for (int i = 0; i < size; i++) {
int viewIndex = mDrawViewIndexMap.get(mFlatViewGroup.getChildAt(i).getId());
if (mStop < viewIndex) {
while (commandIndex < mStop) {
mDrawCommands[commandIndex++].draw(mFlatViewGroup, canvas);
}
// else, don't draw, and don't increment index
} else {
drawCommand.draw(mFlatViewGroup, canvas);
// We are now out of commands to draw, so we can just draw the remaining attached children.
mDrawCommands[viewIndex].draw(mFlatViewGroup, canvas);
while (++i != size) {
viewIndex = mDrawViewIndexMap.get(mFlatViewGroup.getChildAt(i).getId());
mDrawCommands[viewIndex].draw(mFlatViewGroup, canvas);
}
// Everything is drawn, lets get out of here.
return;
} else if (commandIndex <= viewIndex) {
while (commandIndex < viewIndex) {
mDrawCommands[commandIndex++].draw(mFlatViewGroup, canvas);
}
// Command index now == viewIndex, so increment beyond it.
commandIndex++;
}
mDrawCommands[viewIndex].draw(mFlatViewGroup, canvas);
}
// We have drawn all the views, now just draw the remaining draw commands.
while (commandIndex < mStop) {
mDrawCommands[commandIndex++].draw(mFlatViewGroup, canvas);
}
}
@Override
void debugDraw(Canvas canvas) {
// Draws clipped draw commands, but does not draw clipped views.
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {
if (isNotClipped(((DrawView) drawCommand).reactTag)) {

View File

@ -24,7 +24,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
/**
* Abstract {@link DrawCommandManager} with directional clipping.
*/
/* package */ abstract class DirectionalClippingDrawCommandManager extends DrawCommandManager {
/* package */ abstract class DirectionalClippingDrawCommandManager {
// This will be fixed in the next diff!!!
private final FlatViewGroup mFlatViewGroup;
DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
@ -52,7 +53,6 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
updateClippingRect();
}
@Override
public void mountDrawCommands(DrawCommand[] drawCommands) {
mDrawCommands = drawCommands;
mDrawViewMap.clear();
@ -80,13 +80,12 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
return !mClippedSubviews.containsKey(id);
}
@Override
public void mountViews(ViewResolver viewResolver, int[] viewsToAdd, int[] viewsToDetach) {
for (int viewToAdd : viewsToAdd) {
if (viewToAdd > 0) {
// This view was not previously attached to this parent.
View view = viewResolver.getView(viewToAdd);
ensureViewHasNoParent(view);
// ensureViewHasNoParent(view);
DrawView drawView = Assertions.assertNotNull(mDrawViewMap.get(viewToAdd));
drawView.mWasMounted = true;
if (animating(view) || withinBounds(drawView)) {
@ -100,7 +99,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
// This view was previously attached, and just temporarily detached.
DrawView drawView = Assertions.assertNotNull(mDrawViewMap.get(-viewToAdd));
View view = viewResolver.getView(drawView.reactTag);
ensureViewHasNoParent(view);
// ensureViewHasNoParent(view);
if (drawView.mWasMounted) {
// The DrawView has been mounted before.
if (isNotClipped(drawView.reactTag)) {
@ -161,7 +160,6 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
return !(beforeRect(drawView) || afterRect(drawView));
}
@Override
public boolean updateClippingRect() {
ReactClippingViewGroupHelper.calculateClippingRect(mFlatViewGroup, mClippingRect);
if (mFlatViewGroup.getParent() == null || mClippingRect.top == mClippingRect.bottom) {
@ -201,17 +199,14 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
return needsInvalidate;
}
@Override
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(mClippingRect);
}
@Override
public Collection<View> getDetachedViews() {
return mClippedSubviews.values();
}
@Override
public void draw(Canvas canvas) {
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {
@ -225,7 +220,6 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
}
}
@Override
void debugDraw(Canvas canvas) {
for (DrawCommand drawCommand : mDrawCommands) {
if (drawCommand instanceof DrawView) {

View File

@ -16,7 +16,7 @@ import android.graphics.Canvas;
* Instances of DrawCommand are created in background thread and passed to UI thread.
* Once a DrawCommand is shared with UI thread, it can no longer be mutated in background thread.
*/
public interface DrawCommand {
public abstract class DrawCommand {
// used by StateBuilder, FlatViewGroup and FlatShadowNode
/* package */ static final DrawCommand[] EMPTY_ARRAY = new DrawCommand[0];
@ -26,7 +26,7 @@ public interface DrawCommand {
* @param parent The parent to get child information from, if needed
* @param canvas The canvas to draw into
*/
public void draw(FlatViewGroup parent, Canvas canvas);
abstract void draw(FlatViewGroup parent, Canvas canvas);
/**
* Performs debug bounds drawing into the given canvas.
@ -34,5 +34,13 @@ public interface DrawCommand {
* @param parent The parent to get child information from, if needed
* @param canvas The canvas to draw into
*/
public void debugDraw(FlatViewGroup parent, Canvas canvas);
abstract void debugDraw(FlatViewGroup parent, Canvas canvas);
abstract float getLeft();
abstract float getTop();
abstract float getRight();
abstract float getBottom();
}

View File

@ -9,15 +9,18 @@
package com.facebook.react.flat;
import javax.annotation.Nullable;
import java.util.Collection;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewParent;
/**
* Underlying logic behind handling draw commands from {@link FlatViewGroup}.
* Underlying logic behind handling clipping draw commands from {@link FlatViewGroup}.
*/
/* package */ abstract class DrawCommandManager {
@ -27,8 +30,20 @@ import android.view.ViewParent;
* called after by the UIManager.
*
* @param drawCommands The draw commands to mount.
* @param drawViewIndexMap Mapping of ids to index position within the draw command array.
* @param maxBottom At each index i, the maximum bottom value (or right value in the case of
* horizontal clipping) value of all draw commands at or below i.
* @param minTop At each index i, the minimum top value (or left value in the case of horizontal
* clipping) value of all draw commands at or below i.
* @param willMountViews Whether we are going to also receive a mountViews command in this state
* cycle.
*/
abstract void mountDrawCommands(DrawCommand[] drawCommands);
abstract void mountDrawCommands(
DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] maxBottom,
float[] minTop,
boolean willMountViews);
/**
* Add and detach a set of views. The views added here will already have a DrawView passed in
@ -78,6 +93,36 @@ import android.view.ViewParent;
*/
abstract void debugDraw(Canvas canvas);
/**
* Mount node regions, which are the hit boxes of the shadow node children of this FlatViewGroup,
* though some may not have a corresponding draw command.
*
* @param nodeRegions Array of node regions to mount.
* @param maxBottom At each index i, the maximum bottom value (or right value in the case of
* horizontal clipping) value of all node regions at or below i.
* @param minTop At each index i, the minimum top value (or left value in the case of horizontal
* clipping) value of all draw commands at or below i.
*/
abstract void mountNodeRegions(NodeRegion[] nodeRegions, float[] maxBottom, float[] minTop);
/**
* Find a matching node region for a touch.
*
* @param touchX X coordinate of touch.
* @param touchY Y coordinate of touch.
* @return Matching node region, or null if none are found.
*/
abstract @Nullable NodeRegion anyNodeRegionWithinBounds(float touchX, float touchY);
/**
* Find a matching virtual node region for a touch.
*
* @param touchX X coordinate of touch.
* @param touchY Y coordinate of touch.
* @return Matching node region, or null if none are found.
*/
abstract @Nullable NodeRegion virtualNodeRegionWithinBounds(float touchX, float touchY);
/**
* Throw a runtime exception if a view we are trying to attach is already parented.
*
@ -96,16 +141,4 @@ import android.view.ViewParent;
DrawCommand[] drawCommands) {
return new ClippingDrawCommandManager(flatViewGroup, drawCommands);
}
static DrawCommandManager getVerticalClippingInstance(
FlatViewGroup flatViewGroup,
DrawCommand[] drawCommands) {
return new VerticalClippingDrawCommandManager(flatViewGroup, drawCommands);
}
static DrawCommandManager getHorizontalClippingInstance(
FlatViewGroup flatViewGroup,
DrawCommand[] drawCommands) {
return new HorizontalClippingDrawCommandManager(flatViewGroup, drawCommands);
}
}

View File

@ -19,7 +19,7 @@ import com.facebook.react.bridge.ReadableArray;
/**
* Common interface for DrawImageWithPipeline and DrawImageWithDrawee.
*/
/* package */ interface DrawImage extends DrawCommand, AttachDetachListener {
/* package */ interface DrawImage extends AttachDetachListener {
/**
* Returns true if an image source was assigned to the DrawImage.
* A DrawImage with no source will not draw anything.

View File

@ -16,6 +16,7 @@ import android.graphics.Path;
import android.graphics.RectF;
/* package */ final class DrawView extends AbstractDrawCommand {
public static final DrawView[] EMPTY_ARRAY = new DrawView[0];
// the minimum rounded clipping value before we actually do rounded clipping
/* package */ static final float MINIMUM_ROUNDED_CLIPPING_VALUE = 0.5f;
private final RectF TMP_RECT = new RectF();

View File

@ -15,7 +15,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import android.graphics.Rect;
import android.util.SparseIntArray;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
@ -82,6 +82,55 @@ import com.facebook.react.uimanager.ViewManagerRegistry;
}
}
/**
* Updates DrawCommands and AttachDetachListeners of a clipping FlatViewGroup specified by a
* reactTag.
*
* @param reactTag The react tag to lookup FlatViewGroup by.
* @param drawCommands If non-null, new draw commands to execute during the drawing.
* @param drawViewIndexMap Mapping of react tags to the index of the corresponding DrawView
* command in the draw command array.
* @param commandMaxBot At each index i, the maximum bottom value (or right value in the case of
* horizontal clipping) value of all draw commands at or below i.
* @param commandMinTop At each index i, the minimum top value (or left value in the case of
* horizontal clipping) value of all draw commands at or below i.
* @param listeners If non-null, new attach-detach listeners.
* @param nodeRegions Node regions to mount.
* @param regionMaxBot At each index i, the maximum bottom value (or right value in the case of
* horizontal clipping) value of all node regions at or below i.
* @param regionMinTop At each index i, the minimum top value (or left value in the case of
* horizontal clipping) value of all draw commands at or below i.
* @param willMountViews Whether we are going to also send a mountViews command in this state
* cycle.
*/
/* package */ void updateClippingMountState(
int reactTag,
@Nullable DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] commandMaxBot,
float[] commandMinTop,
@Nullable AttachDetachListener[] listeners,
@Nullable NodeRegion[] nodeRegions,
float[] regionMaxBot,
float[] regionMinTop,
boolean willMountViews) {
FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
if (drawCommands != null) {
view.mountClippingDrawCommands(
drawCommands,
drawViewIndexMap,
commandMaxBot,
commandMinTop,
willMountViews);
}
if (listeners != null) {
view.mountAttachDetachListeners(listeners);
}
if (nodeRegions != null) {
view.mountClippingNodeRegions(nodeRegions, regionMaxBot, regionMinTop);
}
}
/* package */ void updateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
View view = resolveView(reactTag);
if (view instanceof FlatViewGroup) {

View File

@ -41,8 +41,9 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
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 =
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.
@ -542,4 +543,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper;
/* package */ final void signalBackingViewIsCreated() {
mBackingViewIsCreated = true;
}
public boolean clipsSubviews() {
return false;
}
}

View File

@ -11,7 +11,7 @@ package com.facebook.react.flat;
import javax.annotation.Nullable;
import android.graphics.Rect;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
@ -73,6 +73,61 @@ import com.facebook.react.uimanager.UIViewOperationQueue;
}
}
/**
* UIOperation that updates DrawCommands for a View defined by reactTag.
*/
private final class UpdateClippingMountState implements UIOperation {
private final int mReactTag;
private final @Nullable DrawCommand[] mDrawCommands;
private final SparseIntArray mDrawViewIndexMap;
private final float[] mCommandMaxBot;
private final float[] mCommandMinTop;
private final @Nullable AttachDetachListener[] mAttachDetachListeners;
private final @Nullable NodeRegion[] mNodeRegions;
private final float[] mRegionMaxBot;
private final float[] mRegionMinTop;
private final boolean mWillMountViews;
private UpdateClippingMountState(
int reactTag,
@Nullable DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] commandMaxBot,
float[] commandMinTop,
@Nullable AttachDetachListener[] listeners,
@Nullable NodeRegion[] nodeRegions,
float[] regionMaxBot,
float[] regionMinTop,
boolean willMountViews) {
mReactTag = reactTag;
mDrawCommands = drawCommands;
mDrawViewIndexMap = drawViewIndexMap;
mCommandMaxBot = commandMaxBot;
mCommandMinTop = commandMinTop;
mAttachDetachListeners = listeners;
mNodeRegions = nodeRegions;
mRegionMaxBot = regionMaxBot;
mRegionMinTop = regionMinTop;
mWillMountViews = willMountViews;
}
@Override
public void execute() {
mNativeViewHierarchyManager.updateClippingMountState(
mReactTag,
mDrawCommands,
mDrawViewIndexMap,
mCommandMaxBot,
mCommandMinTop,
mAttachDetachListeners,
mNodeRegions,
mRegionMaxBot,
mRegionMinTop,
mWillMountViews);
}
}
private final class UpdateViewGroup implements UIOperation {
private final int mReactTag;
@ -359,6 +414,33 @@ import com.facebook.react.uimanager.UIViewOperationQueue;
nodeRegions));
}
/**
* Enqueues a new UIOperation that will update DrawCommands for a View defined by reactTag.
*/
public void enqueueUpdateClippingMountState(
int reactTag,
@Nullable DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] commandMaxBot,
float[] commandMinTop,
@Nullable AttachDetachListener[] listeners,
@Nullable NodeRegion[] nodeRegions,
float[] regionMaxBot,
float[] regionMinTop,
boolean willMountViews) {
enqueueUIOperation(new UpdateClippingMountState(
reactTag,
drawCommands,
drawViewIndexMap,
commandMaxBot,
commandMinTop,
listeners,
nodeRegions,
regionMaxBot,
regionMinTop,
willMountViews));
}
public void enqueueUpdateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
enqueueUIOperation(new UpdateViewGroup(reactTag, viewsToAdd, viewsToDetach));
}

View File

@ -22,11 +22,13 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.touch.OnInterceptTouchEventListener;
@ -90,7 +92,6 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
private static final ArrayList<FlatViewGroup> LAYOUT_REQUESTS = new ArrayList<>();
private static final Rect VIEW_BOUNDS = new Rect();
private static final Rect EMPTY_RECT = new Rect();
private @Nullable InvalidateCallback mInvalidateCallback;
private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
@ -565,11 +566,22 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
}
/* package */ void mountDrawCommands(DrawCommand[] drawCommands) {
if (mDrawCommandManager != null) {
mDrawCommandManager.mountDrawCommands(drawCommands);
} else {
mDrawCommands = drawCommands;
}
mDrawCommands = drawCommands;
invalidate();
}
/* package */ void mountClippingDrawCommands(
DrawCommand[] drawCommands,
SparseIntArray drawViewIndexMap,
float[] maxBottom,
float[] minTop,
boolean willMountViews) {
Assertions.assertNotNull(mDrawCommandManager).mountDrawCommands(
drawCommands,
drawViewIndexMap,
maxBottom,
minTop,
willMountViews);
invalidate();
}
@ -646,6 +658,14 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
mNodeRegions = nodeRegions;
}
/* package */ void mountClippingNodeRegions(
NodeRegion[] nodeRegions,
float[] maxBottom,
float[] minTop) {
mNodeRegions = nodeRegions;
Assertions.assertNotNull(mDrawCommandManager).mountNodeRegions(nodeRegions, maxBottom, minTop);
}
/**
* Mount a list of views to add, and dismount a list of views to detach. Ids will not appear in
* both lists, aka:
@ -712,6 +732,10 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams()));
}
/* package */ void attachViewToParent(View view, int index) {
attachViewToParent(view, index, ensureLayoutParams(view.getLayoutParams()));
}
private void processLayoutRequest() {
mIsLayoutRequested = false;
for (int i = 0, childCount = getChildCount(); i != childCount; ++i) {
@ -769,6 +793,9 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
}
private @Nullable NodeRegion virtualNodeRegionWithinBounds(float touchX, float touchY) {
if (mDrawCommandManager != null) {
return mDrawCommandManager.virtualNodeRegionWithinBounds(touchX, touchY);
}
for (int i = mNodeRegions.length - 1; i >= 0; --i) {
NodeRegion nodeRegion = mNodeRegions[i];
if (!nodeRegion.mIsVirtual) {
@ -784,6 +811,9 @@ import com.facebook.react.views.view.ReactClippingViewGroup;
}
private @Nullable NodeRegion anyNodeRegionWithinBounds(float touchX, float touchY) {
if (mDrawCommandManager != null) {
return mDrawCommandManager.anyNodeRegionWithinBounds(touchX, touchY);
}
for (int i = mNodeRegions.length - 1; i >= 0; --i) {
NodeRegion nodeRegion = mNodeRegions[i];
if (nodeRegion.withinBounds(touchX, touchY)) {

View File

@ -13,6 +13,7 @@ import javax.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
@ -21,6 +22,23 @@ import com.facebook.react.uimanager.annotations.ReactPropGroup;
private @Nullable DrawBorder mDrawBorder;
boolean mRemoveClippedSubviews;
boolean mHorizontal;
@Override
/* package */ void handleUpdateProperties(ReactStylesDiffMap styles) {
mRemoveClippedSubviews = mRemoveClippedSubviews ||
(styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS) &&
styles.getBoolean(PROP_REMOVE_CLIPPED_SUBVIEWS, false));
if (mRemoveClippedSubviews) {
mHorizontal = mHorizontal ||
(styles.hasKey(PROP_HORIZONTAL) && styles.getBoolean(PROP_HORIZONTAL, false));
}
super.handleUpdateProperties(styles);
}
@Override
protected void collectState(
StateBuilder stateBuilder,
@ -119,4 +137,9 @@ import com.facebook.react.uimanager.annotations.ReactPropGroup;
invalidate();
return mDrawBorder;
}
@Override
public boolean clipsSubviews() {
return mRemoveClippedSubviews;
}
}

View File

@ -13,6 +13,9 @@ import javax.annotation.Nullable;
import java.util.ArrayList;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.facebook.csslayout.Spacing;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.OnLayoutEvent;
@ -28,6 +31,9 @@ import com.facebook.react.uimanager.events.EventDispatcher;
* that Android finally can display.
*/
/* package */ final class StateBuilder {
/* package */ static final float[] EMPTY_FLOAT_ARRAY = new float[0];
/* package */ static final SparseArray<DrawView> EMPTY_SPARSE_DRAWVIEW = new SparseArray<>();
/* package */ static final SparseIntArray EMPTY_SPARSE_INT = new SparseIntArray();
private static final boolean SKIP_UP_TO_DATE_NODES = true;
@ -329,12 +335,86 @@ import com.facebook.react.uimanager.events.EventDispatcher;
node.updateOverflowsContainer();
}
// We need to finish the native children so that we can process clipping FlatViewGroup.
final FlatShadowNode[] nativeChildren = mNativeChildren.finish();
if (shouldUpdateMountState) {
mOperationsQueue.enqueueUpdateMountState(
node.getReactTag(),
drawCommands,
listeners,
nodeRegions);
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];
float last = 0;
// Loop through the DrawCommands, keeping track of the maximum y we've seen if we only
// iterated through items up to this position
for (int i = 0; i < drawCommands.length; i++) {
if (drawCommands[i] instanceof DrawView) {
DrawView drawView = (DrawView) drawCommands[i];
// These will generally be roughly sorted by id, so try to insert at the end if
// possible.
drawViewIndexMap.append(drawView.reactTag, i);
last = Math.max(last, drawView.mLogicalBottom);
} else {
last = Math.max(last, drawCommands[i].getBottom());
}
commandMaxBottom[i] = last;
}
// Intentionally leave last as it was, since it's at the maximum bottom position we've
// seen so far, we can use it again.
// Loop through the DrawCommands backwards, keeping track of the minimum y we've seen at
// this position
for (int i = drawCommands.length - 1; i >= 0; i--) {
if (drawCommands[i] instanceof DrawView) {
last = Math.min(last, ((DrawView) drawCommands[i]).mLogicalTop);
} else {
last = Math.min(last, drawCommands[i].getTop());
}
commandMinTop[i] = last;
}
}
float[] regionMaxBottom = EMPTY_FLOAT_ARRAY;
float[] regionMinTop = EMPTY_FLOAT_ARRAY;
if (nodeRegions != null) {
regionMaxBottom = new float[nodeRegions.length];
regionMinTop = new float[nodeRegions.length];
float last = 0;
for (int i = 0; i < nodeRegions.length; i++) {
last = Math.max(last, nodeRegions[i].mBottom);
regionMaxBottom[i] = last;
}
for (int i = nodeRegions.length - 1; i >= 0; i--) {
last = Math.min(last, nodeRegions[i].mTop);
regionMinTop[i] = last;
}
}
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()) {
@ -342,7 +422,6 @@ import com.facebook.react.uimanager.events.EventDispatcher;
node.markUpdateSeen();
}
final FlatShadowNode[] nativeChildren = mNativeChildren.finish();
if (nativeChildren != null) {
updateNativeChildren(node, node.getNativeChildren(), nativeChildren);
}