From 5f162ca1192fa4ee275f07d3756d134317facfc9 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 13 May 2016 17:49:34 -0700 Subject: [PATCH] Implement RemoveClippedSubviews for Nodes Summary: RN has an optimization in which a ScrollView (or similar ViewGroups) can ask to remove clipped subviews from the View hierarchy. This patch implements this optimization for Nodes, but instead of adding and removing the Views, it attaches and detaches Views instead. Note that this patch does not handle overflow: visible. This is addressed in a stacked patch on top of this patch (to simplify the review process). Reviewed By: astreet Differential Revision: D3235050 --- .../com/facebook/react/flat/DrawView.java | 6 +- .../facebook/react/flat/FlatShadowNode.java | 10 +- .../react/flat/FlatUIViewOperationQueue.java | 12 +- .../facebook/react/flat/FlatViewGroup.java | 161 +++++++++++++++++- .../facebook/react/flat/RCTViewManager.java | 8 +- 5 files changed, 185 insertions(+), 12 deletions(-) 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 8d46dd049..9fdad200d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java @@ -13,9 +13,11 @@ import android.graphics.Canvas; /* package */ final class DrawView extends AbstractClippingDrawCommand { - /* package */ static final DrawView INSTANCE = new DrawView(0, 0, 0, 0); + /* package */ final int reactTag; + /* package */ boolean isViewGroupClipped; - public DrawView(float clipLeft, float clipTop, float clipRight, float clipBottom) { + public DrawView(int reactTag, float clipLeft, float clipTop, float clipRight, float clipBottom) { + this.reactTag = reactTag; setClipBounds(clipLeft, clipTop, clipRight, clipBottom); } 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 b73b533d8..139b2b274 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -18,6 +18,7 @@ import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.views.view.ReactClippingViewGroupHelper; /** * FlatShadowNode is a base class for all shadow node used in FlatUIImplementation. It extends @@ -36,6 +37,8 @@ import com.facebook.react.uimanager.annotations.ReactProp; 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 = + ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS; private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY; private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY; @@ -74,7 +77,8 @@ import com.facebook.react.uimanager.annotations.ReactProp; styles.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE) || styles.hasKey(PROP_ACCESSIBILITY_LIVE_REGION) || styles.hasKey(PROP_TRANSFORM) || - styles.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY)) { + styles.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY) || + styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) { forceMountToView(); } } @@ -317,7 +321,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; } if (mDrawView == null) { - mDrawView = DrawView.INSTANCE; + mDrawView = new DrawView(getReactTag(), 0, 0, 0, 0); invalidate(); // reset NodeRegion to allow it getting garbage-collected @@ -327,7 +331,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; /* package */ final DrawView collectDrawView(float left, float top, float right, float bottom) { if (!Assertions.assumeNotNull(mDrawView).clipBoundsMatch(left, top, right, bottom)) { - mDrawView = new DrawView(left, top, right, bottom); + mDrawView = new DrawView(getReactTag(), left, top, right, bottom); } return mDrawView; 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 dce0de70b..549857359 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatUIViewOperationQueue.java @@ -13,6 +13,7 @@ import javax.annotation.Nullable; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.NoSuchNativeViewException; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.UIViewOperationQueue; @@ -180,8 +181,15 @@ import com.facebook.react.uimanager.UIViewOperationQueue; @Override public void execute() { - // Measure native View - mNativeViewHierarchyManager.measure(mReactTag, MEASURE_BUFFER); + try { + // Measure native View + mNativeViewHierarchyManager.measure(mReactTag, MEASURE_BUFFER); + } catch (NoSuchNativeViewException noSuchNativeViewException) { + // Invoke with no args to signal failure and to allow JS to clean up the callback + // handle. + mCallback.invoke(); + return; + } float nativeViewX = MEASURE_BUFFER[0]; float nativeViewY = MEASURE_BUFFER[1]; 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 df2e8cdd8..4ef0bf818 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -13,6 +13,8 @@ import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import android.content.Context; import android.graphics.Canvas; @@ -22,7 +24,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.animation.Animation; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.common.SystemClock; @@ -33,13 +37,16 @@ import com.facebook.react.uimanager.ReactCompoundViewGroup; import com.facebook.react.uimanager.ReactPointerEventsView; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.views.image.ImageLoadEvent; +import com.facebook.react.views.view.ReactClippingViewGroup; +import com.facebook.react.views.view.ReactClippingViewGroupHelper; /** * A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over * array of DrawCommands, executing them one by one. */ /* package */ final class FlatViewGroup extends ViewGroup - implements ReactInterceptingViewGroup, ReactCompoundViewGroup, ReactPointerEventsView { + implements ReactInterceptingViewGroup, ReactClippingViewGroup, + ReactCompoundViewGroup, ReactPointerEventsView { /** * Helper class that allows AttachDetachListener to invalidate the hosting View. */ @@ -88,6 +95,12 @@ import com.facebook.react.views.image.ImageLoadEvent; private long mLastTouchDownTime; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; + private boolean mRemoveClippedSubviews; + private @Nullable Rect mClippingRect; + // lookups in o(1) instead of o(log n) - trade space for time + private final Map mDrawViewMap = new HashMap<>(); + private final Map mClippedSubviews = new HashMap<>(); + /* package */ FlatViewGroup(Context context) { super(context); setClipChildren(false); @@ -144,8 +157,21 @@ import com.facebook.react.views.image.ImageLoadEvent; public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - for (DrawCommand drawCommand : mDrawCommands) { - drawCommand.draw(this, canvas); + if (mRemoveClippedSubviews) { + for (DrawCommand drawCommand : mDrawCommands) { + if (drawCommand instanceof DrawView) { + if (!((DrawView) drawCommand).isViewGroupClipped) { + drawCommand.draw(this, canvas); + } + // else, don't draw, and don't increment index + } else { + drawCommand.draw(this, canvas); + } + } + } else { + for (DrawCommand drawCommand : mDrawCommands) { + drawCommand.draw(this, canvas); + } } if (mDrawChildIndex != getChildCount()) { @@ -187,6 +213,10 @@ import com.facebook.react.views.image.ImageLoadEvent; super.onAttachedToWindow(); dispatchOnAttached(mAttachDetachListeners); + + if (mRemoveClippedSubviews) { + updateClippingRect(); + } } @Override @@ -207,6 +237,10 @@ import com.facebook.react.views.image.ImageLoadEvent; mHotspot.setBounds(0, 0, w, h); invalidate(); } + + if (mRemoveClippedSubviews) { + updateClippingRect(); + } } @Override @@ -358,6 +392,15 @@ import com.facebook.react.views.image.ImageLoadEvent; /* package */ void mountDrawCommands(DrawCommand[] drawCommands) { mDrawCommands = drawCommands; + if (mRemoveClippedSubviews) { + mDrawViewMap.clear(); + for (DrawCommand drawCommand : mDrawCommands) { + if (drawCommand instanceof DrawView) { + DrawView drawView = (DrawView) drawCommand; + mDrawViewMap.put(drawView.reactTag, drawView); + } + } + } invalidate(); } @@ -393,11 +436,27 @@ import com.facebook.react.views.image.ImageLoadEvent; } else { View view = ensureViewHasNoParent(viewResolver.getView(-viewToAdd)); attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams())); + if (mRemoveClippedSubviews) { + mClippedSubviews.remove(-viewToAdd); + DrawView drawView = mDrawViewMap.get(-viewToAdd); + if (drawView != null) { + drawView.isViewGroupClipped = false; + } + } } } for (int viewToDetach : viewsToDetach) { - removeDetachedView(viewResolver.getView(viewToDetach), false); + View view = viewResolver.getView(viewToDetach); + if (view.getParent() != null) { + removeViewInLayout(view); + } else { + removeDetachedView(view, false); + } + + if (mRemoveClippedSubviews) { + mClippedSubviews.remove(viewToDetach); + } } invalidate(); @@ -493,4 +552,98 @@ import com.facebook.react.views.image.ImageLoadEvent; } return generateDefaultLayoutParams(); } + + @Override + public void updateClippingRect() { + if (!mRemoveClippedSubviews) { + return; + } + + Assertions.assertNotNull(mClippingRect); + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + if (getParent() != null && mClippingRect.top != mClippingRect.bottom) { + updateClippingToRect(mClippingRect); + } + } + + private void updateClippingToRect(Rect clippingRect) { + int index = 0; + boolean needsInvalidate = false; + for (DrawCommand drawCommand : mDrawCommands) { + if (drawCommand instanceof DrawView) { + DrawView drawView = (DrawView) drawCommand; + FlatViewGroup flatViewGroup = mClippedSubviews.get(drawView.reactTag); + if (flatViewGroup != null) { + // invisible + if (clippingRect.intersects( + flatViewGroup.getLeft(), + flatViewGroup.getTop(), + flatViewGroup.getRight(), + flatViewGroup.getBottom())) { + // now on the screen + attachViewToParent( + flatViewGroup, + index++, + ensureLayoutParams(flatViewGroup.getLayoutParams())); + mClippedSubviews.remove(flatViewGroup.getId()); + drawView.isViewGroupClipped = false; + needsInvalidate = true; + } + } else { + // visible + View view = getChildAt(index++); + if (view instanceof FlatViewGroup) { + FlatViewGroup flatChildView = (FlatViewGroup) view; + Animation animation = flatChildView.getAnimation(); + boolean isAnimating = animation != null && !animation.hasEnded(); + if (!isAnimating && + !clippingRect.intersects( + view.getLeft(), + view.getTop(), + view.getRight(), + view.getBottom())) { + // now off the screen + mClippedSubviews.put(view.getId(), flatChildView); + detachViewFromParent(view); + drawView.isViewGroupClipped = true; + index--; + needsInvalidate = true; + } + } + } + } + } + + if (needsInvalidate) { + invalidate(); + } + } + + @Override + public void getClippingRect(Rect outClippingRect) { + outClippingRect.set(mClippingRect); + } + + @Override + public void setRemoveClippedSubviews(boolean removeClippedSubviews) { + if (removeClippedSubviews == mRemoveClippedSubviews) { + return; + } + mRemoveClippedSubviews = removeClippedSubviews; + if (removeClippedSubviews) { + mClippingRect = new Rect(); + updateClippingRect(); + } else { + // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener + Assertions.assertNotNull(mClippingRect); + getDrawingRect(mClippingRect); + updateClippingToRect(mClippingRect); + mClippingRect = null; + } + } + + @Override + public boolean getRemoveClippedSubviews() { + return mRemoveClippedSubviews; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTViewManager.java index c4cd5278b..1244669e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTViewManager.java @@ -21,8 +21,9 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.PointerEvents; -import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.views.view.ReactClippingViewGroupHelper; import com.facebook.react.views.view.ReactDrawableHelper; /** @@ -102,6 +103,11 @@ import com.facebook.react.views.view.ReactDrawableHelper; view.setPointerEvents(parsePointerEvents(pointerEventsStr)); } + @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) + public void setRemoveClippedSubviews(FlatViewGroup view, boolean removeClippedSubviews) { + view.setRemoveClippedSubviews(removeClippedSubviews); + } + private static PointerEvents parsePointerEvents(@Nullable String pointerEventsStr) { if (pointerEventsStr != null) { switch (pointerEventsStr) {