From 192c99a4f6382038101d65bae56677e3cc8ae211 Mon Sep 17 00:00:00 2001 From: Seth Kirby Date: Mon, 8 Aug 2016 16:05:36 -0700 Subject: [PATCH] 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 --- .../react/flat/AbstractDrawCommand.java | 2 +- .../flat/ClippingDrawCommandManager.java | 311 ++++++++++++++---- ...DirectionalClippingDrawCommandManager.java | 14 +- .../com/facebook/react/flat/DrawCommand.java | 14 +- .../react/flat/DrawCommandManager.java | 61 +++- .../com/facebook/react/flat/DrawImage.java | 2 +- .../com/facebook/react/flat/DrawView.java | 1 + .../flat/FlatNativeViewHierarchyManager.java | 51 ++- .../facebook/react/flat/FlatShadowNode.java | 7 +- .../react/flat/FlatUIViewOperationQueue.java | 84 ++++- .../facebook/react/flat/FlatViewGroup.java | 42 ++- .../java/com/facebook/react/flat/RCTView.java | 23 ++ .../com/facebook/react/flat/StateBuilder.java | 91 ++++- 13 files changed, 598 insertions(+), 105 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java index b1d671b75..a3be1cbac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java @@ -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; diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java index 74ef46adb..4ec802df8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java @@ -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 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 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 mViewsToRemove = new SparseArray<>(); + private final ArrayList 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)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java index 2ef9838b5..0c773e118 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java @@ -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 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) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommand.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommand.java index 3a79338a4..e88436fb3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommand.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommand.java @@ -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(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java index 050967439..2079079d2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java @@ -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); - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java index 96c9d4dc0..6679ab1e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java @@ -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. diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java index 2b5036c7b..1a57a0219 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java @@ -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(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java index 2786cb7bf..042be375a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java @@ -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) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java index 25457424f..98d38628b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -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; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java index 8753db095..ef13a0f9f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java @@ -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)); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java index c2b468aa1..d229b7c7d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -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 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)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java index 97e6f8a16..33cf11ffd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java @@ -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; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java index 2cfd51353..41ebd3def 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -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 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); }