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) {