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 f847115f1..74ef46adb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ClippingDrawCommandManager.java @@ -71,6 +71,10 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; return mClippedSubviews.containsKey(id); } + private boolean isNotClipped(int id) { + return !mClippedSubviews.containsKey(id); + } + @Override public void mountViews(ViewResolver viewResolver, int[] viewsToAdd, int[] viewsToDetach) { for (int viewToAdd : viewsToAdd) { @@ -94,7 +98,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; ensureViewHasNoParent(view); if (drawView.mWasMounted) { // The DrawView has been mounted before. - if (!isClipped(drawView.reactTag)) { + if (isNotClipped(drawView.reactTag)) { // The DrawView is not clipped. Attach it. mFlatViewGroup.attachViewToParent(view); } @@ -118,7 +122,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; } } else { // View should be clipped. - if (!isClipped(drawView.reactTag)) { + if (isNotClipped(drawView.reactTag)) { // View was onscreen. mFlatViewGroup.removeDetachedView(view); clip(drawView.reactTag, view); @@ -147,31 +151,13 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; return animation != null && !animation.hasEnded(); } - // Return true if a view is currently onscreen. - boolean withinBounds(View view) { - if (view instanceof FlatViewGroup) { - FlatViewGroup flatChildView = (FlatViewGroup) view; - return mClippingRect.intersects( - flatChildView.getLeft() + flatChildView.mLogicalAdjustments.left, - flatChildView.getTop() + flatChildView.mLogicalAdjustments.top, - flatChildView.getRight() + flatChildView.mLogicalAdjustments.right, - flatChildView.getBottom() + flatChildView.mLogicalAdjustments.bottom); - } else { - return mClippingRect.intersects( - view.getLeft(), - view.getTop(), - view.getRight(), - view.getBottom()); - } - } - // Return true if a DrawView is currently onscreen. boolean withinBounds(DrawView drawView) { return mClippingRect.intersects( - Math.round(drawView.getLeft()), - Math.round(drawView.getTop()), - Math.round(drawView.getRight()), - Math.round(drawView.getBottom())); + drawView.mLogicalLeft, + drawView.mLogicalTop, + drawView.mLogicalRight, + drawView.mLogicalBottom); } @Override @@ -192,7 +178,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; if (view == null) { // Not clipped, visible view = mFlatViewGroup.getChildAt(index++); - if (!animating(view) && !withinBounds(view)) { + 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); @@ -201,7 +187,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; } 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(view)) { + if (withinBounds(drawView)) { // Now on the screen. Invalidate as we have a new element to draw. unclip(drawView.reactTag); mFlatViewGroup.addViewInLayout(view, index++); @@ -228,7 +214,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; public void draw(Canvas canvas) { for (DrawCommand drawCommand : mDrawCommands) { if (drawCommand instanceof DrawView) { - if (!isClipped(((DrawView) drawCommand).reactTag)) { + if (isNotClipped(((DrawView) drawCommand).reactTag)) { drawCommand.draw(mFlatViewGroup, canvas); } // else, don't draw, and don't increment index @@ -242,7 +228,7 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; void debugDraw(Canvas canvas) { for (DrawCommand drawCommand : mDrawCommands) { if (drawCommand instanceof DrawView) { - if (!isClipped(((DrawView) drawCommand).reactTag)) { + if (isNotClipped(((DrawView) drawCommand).reactTag)) { drawCommand.debugDraw(mFlatViewGroup, canvas); } // else, don't draw, and don't increment index diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java new file mode 100644 index 000000000..2ef9838b5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DirectionalClippingDrawCommandManager.java @@ -0,0 +1,241 @@ +/** + * 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 extends DrawCommandManager { + 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(); + } + + @Override + 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); + } + + @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); + 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)); + } + + @Override + 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; + } + + @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) { + if (isNotClipped(((DrawView) drawCommand).reactTag)) { + drawCommand.draw(mFlatViewGroup, canvas); + } + // else, don't draw, and don't increment index + } else { + drawCommand.draw(mFlatViewGroup, canvas); + } + } + } + + @Override + 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 e51f2751d..050967439 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawCommandManager.java @@ -90,4 +90,22 @@ import android.view.ViewParent; "Cannot add view " + view + " to DrawCommandManager while it has a parent " + oldParent); } } + + static DrawCommandManager getClippingInstance( + FlatViewGroup flatViewGroup, + 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/DrawImageWithPipeline.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java index a8a0b84e0..b361171f5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java @@ -125,6 +125,8 @@ import com.facebook.react.views.imagehelper.MultiSourceHelper.MultiSourceResult; */ @Override protected void onPreDraw(FlatViewGroup parent, Canvas canvas) { + super.onPreDraw(parent, canvas); + Bitmap bitmap = Assertions.assumeNotNull(mRequestHelper).getBitmap(); if (bitmap == null) { mFirstDrawTime = 0; @@ -168,7 +170,7 @@ import com.facebook.react.views.imagehelper.MultiSourceHelper.MultiSourceResult; PAINT.setShader(mBitmapShader); canvas.drawPath(getPathForRoundedBitmap(), PAINT); } - bitmap = null; + drawBorders(canvas); } 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 2775cf5ce..e8c49c757 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java @@ -36,6 +36,14 @@ 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. + /* package */ int mLogicalLeft; + /* package */ int mLogicalTop; + /* package */ int mLogicalRight; + /* package */ int mLogicalBottom; + public DrawView(int reactTag) { this.reactTag = reactTag; } @@ -52,6 +60,10 @@ import android.graphics.RectF; float top, float right, float bottom, + int logicalLeft, + int logicalTop, + int logicalRight, + int logicalBottom, float clipLeft, float clipTop, float clipRight, @@ -68,7 +80,9 @@ import android.graphics.RectF; clipRight, clipBottom); boolean clipRadiusChanged = Math.abs(mClipRadius - clipRadius) > 0.001f; - if (clipRadiusChanged && drawView == this) { + boolean logicalBoundsChanged = + !logicalBoundsMatch(logicalLeft, logicalTop, logicalRight, logicalBottom); + if (drawView == this && (clipRadiusChanged || logicalBoundsChanged)) { // everything matches except the clip radius, so we clone the old one so that we can update // the clip radius in the block below. try { @@ -88,6 +102,10 @@ import android.graphics.RectF; drawView.mPath = null; } + if (logicalBoundsChanged) { + drawView.setLogicalBounds(logicalLeft, logicalTop, logicalRight, logicalBottom); + } + // It is very important that we unset this, as our spec is that newly created DrawViews are // handled differently by the FlatViewGroup. This is needed because updateBoundsAndFreeze // uses .clone(), so we maintain the previous state. @@ -96,6 +114,19 @@ import android.graphics.RectF; return drawView; } + private boolean logicalBoundsMatch(int left, int top, int right, int bottom) { + return left == mLogicalLeft && top == mLogicalTop && + right == mLogicalRight && bottom == mLogicalBottom; + } + + private void setLogicalBounds(int left, int top, int right, int bottom) { + // Do rounding up front and off of the UI thread. + mLogicalLeft = left; + mLogicalTop = top; + mLogicalRight = right; + mLogicalBottom = bottom; + } + @Override public void draw(FlatViewGroup parent, Canvas canvas) { onPreDraw(parent, canvas); 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 9c5aae563..1426f7c70 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -501,6 +501,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), clipLeft, clipTop, clipRight, 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 d5db547bb..f5b6ca987 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -873,14 +873,14 @@ import com.facebook.react.views.view.ReactClippingViewGroup; // We aren't changing state, so don't do anything. return; } - if (currentlyClipping && !removeClippedSubviews) { + if (currentlyClipping) { // Trying to go from a clipping to a non-clipping state, not currently supported by Nodes. // If this is an issue, let us know, but currently there does not seem to be a good case for // supporting this. throw new RuntimeException( "Trying to transition FlatViewGroup from clipping to non-clipping state"); } - mDrawCommandManager = new ClippingDrawCommandManager(this, mDrawCommands); + mDrawCommandManager = DrawCommandManager.getClippingInstance(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 new file mode 100644 index 000000000..2a71211a1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/HorizontalClippingDrawCommandManager.java @@ -0,0 +1,33 @@ +/** + * 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/StateBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java index 0f59af1b4..1bf37ef3c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -536,6 +536,17 @@ import com.facebook.react.uimanager.events.EventDispatcher; ensureBackingViewIsCreated(node); addNativeChild(node); + updated = collectStateForMountableNode( + node, + 0, // left - left + 0, // top - top + right - left, + bottom - top, + parentClipLeft - left, + parentClipTop - top, + parentClipRight - left, + parentClipBottom - top); + if (!parentIsAndroidView) { mDrawCommands.add(node.collectDrawView( left, @@ -548,17 +559,6 @@ import com.facebook.react.uimanager.events.EventDispatcher; parentClipBottom)); } - updated = collectStateForMountableNode( - node, - 0, // left - left - 0, // top - top - right - left, - bottom - top, - parentClipLeft - left, - parentClipTop - top, - parentClipRight - left, - parentClipBottom - top); - if (!needsCustomLayout) { updateViewBounds(node, left, top, right, bottom); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java new file mode 100644 index 000000000..27bc6280c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/VerticalClippingDrawCommandManager.java @@ -0,0 +1,33 @@ +/** + * 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; + } +}