From a4c4a88e27d17f2da127b843c8eee22dac26769b Mon Sep 17 00:00:00 2001 From: Seth Kirby Date: Mon, 8 Aug 2016 17:53:57 -0700 Subject: [PATCH] Support vertical and horizontal clipping with draw command managers. Summary: Adds support for horizontal clipping, though the FlatViewGroup needs to be made aware still of which it is. Reviewed By: ahmedre Differential Revision: D3673501 --- .../flat/ClippingDrawCommandManager.java | 115 +++++---- ...DirectionalClippingDrawCommandManager.java | 235 ------------------ .../react/flat/DrawCommandManager.java | 4 +- .../com/facebook/react/flat/DrawView.java | 29 +-- .../facebook/react/flat/FlatShadowNode.java | 12 +- .../facebook/react/flat/FlatViewGroup.java | 6 +- .../HorizontalClippingDrawCommandManager.java | 33 --- .../flat/HorizontalDrawCommandManager.java | 106 ++++++++ .../com/facebook/react/flat/StateBuilder.java | 46 +--- .../VerticalClippingDrawCommandManager.java | 33 --- .../flat/VerticalDrawCommandManager.java | 108 ++++++++ 11 files changed, 309 insertions(+), 418 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalClippingDrawCommandManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java 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 4ec802df8..b0e4e111f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java @@ -12,7 +12,6 @@ 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; @@ -31,15 +30,15 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; * Implementation of a {@link DrawCommandManager} with clipping. Performs drawing by iterating * over an array of DrawCommands, executing them one by one except when the commands are clipped. */ -/* package */ final class ClippingDrawCommandManager extends DrawCommandManager { +/* package */ abstract class ClippingDrawCommandManager extends DrawCommandManager { private final FlatViewGroup mFlatViewGroup; private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY; - private float[] mCommandMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY; - private float[] mCommandMinTop = StateBuilder.EMPTY_FLOAT_ARRAY; + protected float[] mCommandMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY; + protected float[] mCommandMinTop = StateBuilder.EMPTY_FLOAT_ARRAY; private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY; - private float[] mRegionMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY; - private float[] mRegionMinTop = StateBuilder.EMPTY_FLOAT_ARRAY; + protected float[] mRegionMaxBottom = StateBuilder.EMPTY_FLOAT_ARRAY; + protected float[] mRegionMinTop = StateBuilder.EMPTY_FLOAT_ARRAY; // Onscreen bounds of draw command array. private int mStart; @@ -51,7 +50,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; // Map of views that are currently clipped. private final Map mClippedSubviews = new HashMap<>(); - private final Rect mClippingRect = new Rect(); + protected 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 @@ -74,6 +73,33 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; updateClippingRect(); } + /** + * @return index of the first command that could be on the screen. + */ + abstract int commandStartIndex(); + + /** + * @return index of the first command that is guaranteed to be off the screen, starting from the + * given start. + */ + abstract int commandStopIndex(int start); + + /** + * @return index of the first region that is guaranteed to be outside of the bounds for touch. + */ + abstract int regionStopIndex(float touchX, float touchY); + + /** + * Whether an index and all indices before it are guaranteed to be out of bounds for the current + * touch. + * + * @param index The region index to check. + * @param touchX X coordinate. + * @param touchY Y coordinate. + * @return true if the index and all before it are out of bounds. + */ + abstract boolean regionAboveTouch(int index, float touchX, float touchY); + @Override public void mountDrawCommands( DrawCommand[] drawCommands, @@ -86,20 +112,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; 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; - } + mStart = commandStartIndex(); + mStop = commandStopIndex(mStart); 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 @@ -118,18 +132,14 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; @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; - } + int i = regionStopIndex(touchX, touchY); while (i-- > 0) { NodeRegion nodeRegion = mNodeRegions[i]; if (!nodeRegion.mIsVirtual) { // only interested in virtual nodes continue; } - if (mRegionMaxBottom[i] < touchY) { + if (regionAboveTouch(i, touchX, touchY)) { break; } if (nodeRegion.withinBounds(touchX, touchY)) { @@ -142,14 +152,10 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; @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; - } + int i = regionStopIndex(touchX, touchY); while (i-- > 0) { NodeRegion nodeRegion = mNodeRegions[i]; - if (mRegionMaxBottom[i] < touchY) { + if (regionAboveTouch(i, touchX, touchY)) { break; } if (nodeRegion.withinBounds(touchX, touchY)) { @@ -250,7 +256,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; } // Returns true if a view is currently animating. - static boolean animating(View view) { + private static boolean animating(View view) { Animation animation = view.getAnimation(); return animation != null && !animation.hasEnded(); } @@ -269,22 +275,11 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; return false; } - 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; - } - + int start = commandStartIndex(); + int stop = commandStopIndex(start); if (mStart <= start && stop <= mStop) { + // We would only be removing children, don't invalidate and don't bother changing the + // attached children. return false; } @@ -382,21 +377,24 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; int commandIndex = mStart; int size = mFlatViewGroup.getChildCount(); + // Iterate through the children, making sure that we draw any draw commands we haven't drawn + // that should happen before the next draw view. for (int i = 0; i < size; i++) { + // This is the command index of the next view that we need to draw. Since a view might be + // animating, this view is either before all the commands onscreen, onscreen, or after the + // onscreen commands. int viewIndex = mDrawViewIndexMap.get(mFlatViewGroup.getChildAt(i).getId()); if (mStop < viewIndex) { + // The current view is outside of the viewport bounds. We want to draw all the commands + // up to the stop, then draw all the views outside the viewport bounds. while (commandIndex < mStop) { mDrawCommands[commandIndex++].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; + // We are now out of commands to draw, so we could just draw the remaining attached + // children, but the for loop logic will draw the rest anyway. } else if (commandIndex <= viewIndex) { + // The viewIndex is within our onscreen bounds (or == stop). We want to draw all the + // commands from the current position to the current view, inclusive. while (commandIndex < viewIndex) { mDrawCommands[commandIndex++].draw(mFlatViewGroup, canvas); } @@ -406,7 +404,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; mDrawCommands[viewIndex].draw(mFlatViewGroup, canvas); } - // We have drawn all the views, now just draw the remaining draw commands. + // If we get here, it means we have drawn all the views, now just draw the remaining draw + // commands. while (commandIndex < mStop) { mDrawCommands[commandIndex++].draw(mFlatViewGroup, canvas); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java deleted file mode 100644 index 0c773e118..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java +++ /dev/null @@ -1,235 +0,0 @@ -/** - * 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 java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.view.View; -import android.view.animation.Animation; - -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.views.view.ReactClippingViewGroupHelper; - -/** - * Abstract {@link DrawCommandManager} with directional clipping. - */ -/* package */ abstract class DirectionalClippingDrawCommandManager { - // This will be fixed in the next diff!!! - private final FlatViewGroup mFlatViewGroup; - DrawCommand[] mDrawCommands = DrawCommand.EMPTY_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 final Map mClippedSubviews = new HashMap<>(); - - protected final Rect mClippingRect = new Rect(); - - abstract boolean beforeRect(DrawView drawView); - - abstract boolean afterRect(DrawView drawView); - - /* package */ DirectionalClippingDrawCommandManager( - FlatViewGroup flatViewGroup, - DrawCommand[] drawCommands) { - mFlatViewGroup = flatViewGroup; - initialSetup(drawCommands); - } - - private void initialSetup(DrawCommand[] drawCommands) { - mountDrawCommands(drawCommands); - updateClippingRect(); - } - - public void mountDrawCommands(DrawCommand[] drawCommands) { - mDrawCommands = drawCommands; - mDrawViewMap.clear(); - for (DrawCommand drawCommand : mDrawCommands) { - if (drawCommand instanceof DrawView) { - DrawView drawView = (DrawView) drawCommand; - mDrawViewMap.put(drawView.reactTag, drawView); - } - } - } - - private void clip(int id, View view) { - mClippedSubviews.put(id, view); - } - - private void unclip(int id) { - mClippedSubviews.remove(id); - } - - private boolean isClipped(int id) { - return mClippedSubviews.containsKey(id); - } - - private boolean isNotClipped(int id) { - return !mClippedSubviews.containsKey(id); - } - - 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); - DrawView drawView = Assertions.assertNotNull(mDrawViewMap.get(viewToAdd)); - drawView.mWasMounted = true; - if (animating(view) || withinBounds(drawView)) { - // View should be drawn. This view can't currently be clipped because it wasn't - // previously attached to this parent. - mFlatViewGroup.addViewInLayout(view); - } else { - clip(drawView.reactTag, view); - } - } 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)) { - // The DrawView is not clipped. Attach it. - mFlatViewGroup.attachViewToParent(view); - } - // else The DrawView has been previously mounted and is clipped, so don't attach it. - } else { - // We are mounting it, so lets get this part out of the way. - drawView.mWasMounted = true; - // 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)) { - // View should be drawn. - if (isClipped(drawView.reactTag)) { - // View was clipped, so add it. - mFlatViewGroup.addViewInLayout(view); - unclip(drawView.reactTag); - } else { - // View was just temporarily removed, so attach it. We already know it isn't clipped, - // so no need to unclip it. - mFlatViewGroup.attachViewToParent(view); - } - } else { - // View should be clipped. - if (isNotClipped(drawView.reactTag)) { - // View was onscreen. - mFlatViewGroup.removeDetachedView(view); - clip(drawView.reactTag, view); - } - // else view is already clipped and not within bounds. - } - } - } - } - - for (int viewToDetach : viewsToDetach) { - View view = viewResolver.getView(viewToDetach); - if (view.getParent() != null) { - throw new RuntimeException("Trying to remove view not owned by FlatViewGroup"); - } else { - mFlatViewGroup.removeDetachedView(view); - } - // The view isn't clipped anymore, but gone entirely. - unclip(viewToDetach); - } - } - - // Returns true if a view is currently animating. - static boolean animating(View view) { - Animation animation = view.getAnimation(); - return animation != null && !animation.hasEnded(); - } - - // Return true if a DrawView is currently onscreen. - boolean withinBounds(DrawView drawView) { - return !(beforeRect(drawView) || afterRect(drawView)); - } - - public boolean updateClippingRect() { - ReactClippingViewGroupHelper.calculateClippingRect(mFlatViewGroup, mClippingRect); - if (mFlatViewGroup.getParent() == null || mClippingRect.top == mClippingRect.bottom) { - // If we are unparented or are clipping to an empty rect, no op. Return false so we don't - // invalidate. - 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; - } - } - } - } - - return needsInvalidate; - } - - public void getClippingRect(Rect outClippingRect) { - outClippingRect.set(mClippingRect); - } - - public Collection getDetachedViews() { - return mClippedSubviews.values(); - } - - public void draw(Canvas canvas) { - for (DrawCommand drawCommand : mDrawCommands) { - if (drawCommand instanceof DrawView) { - if (isNotClipped(((DrawView) drawCommand).reactTag)) { - drawCommand.draw(mFlatViewGroup, canvas); - } - // else, don't draw, and don't increment index - } else { - drawCommand.draw(mFlatViewGroup, canvas); - } - } - } - - void debugDraw(Canvas canvas) { - for (DrawCommand drawCommand : mDrawCommands) { - if (drawCommand instanceof DrawView) { - if (isNotClipped(((DrawView) drawCommand).reactTag)) { - drawCommand.debugDraw(mFlatViewGroup, canvas); - } - // else, don't draw, and don't increment index - } else { - drawCommand.debugDraw(mFlatViewGroup, canvas); - } - } - } -} 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 2079079d2..3df3c69e6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java @@ -136,9 +136,9 @@ import android.view.ViewParent; } } - static DrawCommandManager getClippingInstance( + static DrawCommandManager getVerticalClippingInstance( FlatViewGroup flatViewGroup, DrawCommand[] drawCommands) { - return new ClippingDrawCommandManager(flatViewGroup, drawCommands); + return new VerticalDrawCommandManager(flatViewGroup, drawCommands); } } 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 1a57a0219..b6e05923d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java @@ -37,14 +37,15 @@ import android.graphics.RectF; // the path to clip against if we're doing path clipping for rounded borders. @Nullable private Path mPath; - // These should only ever be set from within the DrawView, their only purpose is to prevent - // excessive rounding on the UI thread in FlatViewGroup, and they are left package protected to - // speed up direct access. For overflow visible, these are the adjusted bounds while taking - // overflowing elements into account. - /* package */ int mLogicalLeft; - /* package */ int mLogicalTop; - /* package */ int mLogicalRight; - /* package */ int mLogicalBottom; + // These should only ever be set from within the DrawView, they serve to provide clipping bounds + // for FlatViewGroups, which have strange clipping when it comes to overflow: visible. They are + // left package protected to speed up direct access. For overflow visible, these are the adjusted + // bounds while taking overflowing elements into account, other wise they are just the regular + // bounds of the view. + /* package */ float mLogicalLeft; + /* package */ float mLogicalTop; + /* package */ float mLogicalRight; + /* package */ float mLogicalBottom; public DrawView(int reactTag) { this.reactTag = reactTag; @@ -62,10 +63,10 @@ import android.graphics.RectF; float top, float right, float bottom, - int logicalLeft, - int logicalTop, - int logicalRight, - int logicalBottom, + float logicalLeft, + float logicalTop, + float logicalRight, + float logicalBottom, float clipLeft, float clipTop, float clipRight, @@ -116,12 +117,12 @@ import android.graphics.RectF; return drawView; } - private boolean logicalBoundsMatch(int left, int top, int right, int bottom) { + private boolean logicalBoundsMatch(float left, float top, float right, float bottom) { return left == mLogicalLeft && top == mLogicalTop && right == mLogicalRight && bottom == mLogicalBottom; } - private void setLogicalBounds(int left, int top, int right, int bottom) { + private void setLogicalBounds(float left, float top, float right, float bottom) { // Do rounding up front and off of the UI thread. mLogicalLeft = left; mLogicalTop = top; 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 98d38628b..bb77a8ce0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -506,10 +506,10 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; top, right, bottom, - Math.round(left + mLogicalOffset.left), - Math.round(top + mLogicalOffset.top), - Math.round(right + mLogicalOffset.right), - Math.round(bottom + mLogicalOffset.bottom), + left + mLogicalOffset.left, + top + mLogicalOffset.top, + right + mLogicalOffset.right, + bottom + mLogicalOffset.bottom, clipLeft, clipTop, clipRight, @@ -547,4 +547,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; public boolean clipsSubviews() { return false; } + + public boolean isHorizontal() { + return false; + } } 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 d229b7c7d..d90d91558 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -876,10 +876,6 @@ import com.facebook.react.views.view.ReactClippingViewGroup; } } - /* package */ void detachView(int index) { - detachViewFromParent(index); - } - @Override public void getClippingRect(Rect outClippingRect) { if (mDrawCommandManager == null) { @@ -905,7 +901,7 @@ import com.facebook.react.views.view.ReactClippingViewGroup; throw new RuntimeException( "Trying to transition FlatViewGroup from clipping to non-clipping state"); } - mDrawCommandManager = DrawCommandManager.getClippingInstance(this, mDrawCommands); + mDrawCommandManager = DrawCommandManager.getVerticalClippingInstance(this, mDrawCommands); mDrawCommands = DrawCommand.EMPTY_ARRAY; // We don't need an invalidate here because this can't cause new views to come onscreen, since // everything was unclipped. diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalClippingDrawCommandManager.java deleted file mode 100644 index 2a71211a1..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalClippingDrawCommandManager.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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; - -/** - * {@link DrawCommandManager} with horizontal clipping (The view scrolls left and right). - */ -/* package */ final class HorizontalClippingDrawCommandManager extends - DirectionalClippingDrawCommandManager { - - /* package */ HorizontalClippingDrawCommandManager( - FlatViewGroup flatViewGroup, - DrawCommand[] drawCommands) { - super(flatViewGroup, drawCommands); - } - - @Override - public boolean beforeRect(DrawView drawView) { - return drawView.mLogicalRight <= mClippingRect.left; - } - - @Override - public boolean afterRect(DrawView drawView) { - return drawView.mLogicalLeft >= mClippingRect.right; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java new file mode 100644 index 000000000..66602619b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalDrawCommandManager.java @@ -0,0 +1,106 @@ +/** + * 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 java.util.Arrays; + +import android.util.SparseIntArray; + +/** + * {@link DrawCommandManager} with horizontal clipping (The view scrolls left and right). + */ +/* package */ final class HorizontalDrawCommandManager extends ClippingDrawCommandManager { + + /* package */ HorizontalDrawCommandManager( + FlatViewGroup flatViewGroup, + DrawCommand[] drawCommands) { + super(flatViewGroup, drawCommands); + } + + @Override + int commandStartIndex() { + int start = Arrays.binarySearch(mCommandMaxBottom, mClippingRect.left); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return start < 0 ? ~start : start; + } + + @Override + int commandStopIndex(int start) { + int stop = Arrays.binarySearch( + mCommandMinTop, + start, + mCommandMinTop.length, + mClippingRect.right); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return stop < 0 ? ~stop : stop; + } + + @Override + int regionStopIndex(float touchX, float touchY) { + int stop = Arrays.binarySearch(mRegionMinTop, touchX + 0.0001f); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return stop < 0 ? ~stop : stop; + } + + @Override + boolean regionAboveTouch(int index, float touchX, float touchY) { + return mRegionMaxBottom[index] < touchX; + } + + // These should never be called from the UI thread, as the reason they exist is to do work off the + // UI thread. + public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxBottom, float[] minTop) { + float last = 0; + for (int i = 0; i < regions.length; i++) { + last = Math.max(last, regions[i].mRight); + maxBottom[i] = last; + } + for (int i = regions.length - 1; i >= 0; i--) { + last = Math.min(last, regions[i].mLeft); + minTop[i] = last; + } + } + + public static void fillMaxMinArrays( + DrawCommand[] commands, + float[] maxBottom, + float[] minTop, + SparseIntArray drawViewIndexMap) { + float last = 0; + // Loop through the DrawCommands, keeping track of the maximum we've seen if we only iterated + // through items up to this position. + for (int i = 0; i < commands.length; i++) { + if (commands[i] instanceof DrawView) { + DrawView drawView = (DrawView) commands[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.mLogicalRight); + } else { + last = Math.max(last, commands[i].getRight()); + } + maxBottom[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 backwards, keeping track of the minimum we've seen at this position. + for (int i = commands.length - 1; i >= 0; i--) { + if (commands[i] instanceof DrawView) { + last = Math.min(last, ((DrawView) commands[i]).mLogicalLeft); + } else { + last = Math.min(last, commands[i].getLeft()); + } + minTop[i] = last; + } + } +} 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 41ebd3def..3b5a6f03b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -351,32 +351,12 @@ import com.facebook.react.uimanager.events.EventDispatcher; 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; + if (node.isHorizontal()) { + HorizontalDrawCommandManager + .fillMaxMinArrays(drawCommands, commandMaxBottom, commandMinTop, drawViewIndexMap); + } else { + VerticalDrawCommandManager + .fillMaxMinArrays(drawCommands, commandMaxBottom, commandMinTop, drawViewIndexMap); } } float[] regionMaxBottom = EMPTY_FLOAT_ARRAY; @@ -385,14 +365,12 @@ import com.facebook.react.uimanager.events.EventDispatcher; 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; + if (node.isHorizontal()) { + HorizontalDrawCommandManager + .fillMaxMinArrays(nodeRegions, regionMaxBottom, regionMinTop); + } else { + VerticalDrawCommandManager + .fillMaxMinArrays(nodeRegions, regionMaxBottom, regionMinTop); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java deleted file mode 100644 index 27bc6280c..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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; - -/** - * {@link DrawCommandManager} with vertical clipping (The view scrolls up and down). - */ -/* package */ final class VerticalClippingDrawCommandManager extends - DirectionalClippingDrawCommandManager { - - /* package */ VerticalClippingDrawCommandManager( - FlatViewGroup flatViewGroup, - DrawCommand[] drawCommands) { - super(flatViewGroup, drawCommands); - } - - @Override - boolean beforeRect(DrawView drawView) { - return drawView.mLogicalBottom <= mClippingRect.top; - } - - @Override - boolean afterRect(DrawView drawView) { - return drawView.mLogicalTop >= mClippingRect.bottom; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java new file mode 100644 index 000000000..8d882242c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalDrawCommandManager.java @@ -0,0 +1,108 @@ +/** + * 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 java.util.Arrays; + +import android.util.SparseIntArray; + +/** + * {@link DrawCommandManager} with vertical clipping (The view scrolls up and down). + */ +/* package */ final class VerticalDrawCommandManager extends ClippingDrawCommandManager { + + /* package */ VerticalDrawCommandManager( + FlatViewGroup flatViewGroup, + DrawCommand[] drawCommands) { + super(flatViewGroup, drawCommands); + } + + @Override + int commandStartIndex() { + int start = Arrays.binarySearch(mCommandMaxBottom, mClippingRect.top); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return start < 0 ? ~start : start; + } + + @Override + int commandStopIndex(int start) { + int stop = Arrays.binarySearch( + mCommandMinTop, + start, + mCommandMinTop.length, + mClippingRect.bottom); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return stop < 0 ? ~stop : stop; + } + + @Override + int regionStopIndex(float touchX, float touchY) { + int stop = Arrays.binarySearch(mRegionMinTop, touchY + 0.0001f); + // We don't care whether we matched or not, but positive indices are helpful. The binary search + // returns ~index in the case that it isn't a match, so reverse that here. + return stop < 0 ? ~stop : stop; + } + + @Override + boolean regionAboveTouch(int index, float touchX, float touchY) { + return mRegionMaxBottom[index] < touchY; + } + + // These should never be called from the UI thread, as the reason they exist is to do work off + // the UI thread. + public static void fillMaxMinArrays(NodeRegion[] regions, float[] maxBot, float[] minTop) { + float last = 0; + for (int i = 0; i < regions.length; i++) { + last = Math.max(last, regions[i].mBottom); + maxBot[i] = last; + } + for (int i = regions.length - 1; i >= 0; i--) { + last = Math.min(last, regions[i].mTop); + minTop[i] = last; + } + } + + // These should never be called from the UI thread, as the reason they exist is to do work off + // the UI thread. + public static void fillMaxMinArrays( + DrawCommand[] commands, + float[] maxBot, + float[] minTop, + SparseIntArray drawViewIndexMap) { + float last = 0; + // Loop through the DrawCommands, keeping track of the maximum we've seen if we only iterated + // through items up to this position. + for (int i = 0; i < commands.length; i++) { + if (commands[i] instanceof DrawView) { + DrawView drawView = (DrawView) commands[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, commands[i].getBottom()); + } + maxBot[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 backwards, keeping track of the minimum we've seen at this position. + for (int i = commands.length - 1; i >= 0; i--) { + if (commands[i] instanceof DrawView) { + last = Math.min(last, ((DrawView) commands[i]).mLogicalTop); + } else { + last = Math.min(last, commands[i].getTop()); + } + minTop[i] = last; + } + } +}