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
This commit is contained in:
Ahmed El-Helw 2016-05-13 17:49:34 -07:00
parent 96cb8165c8
commit 5f162ca119
5 changed files with 185 additions and 12 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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];

View File

@ -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<Integer, DrawView> mDrawViewMap = new HashMap<>();
private final Map<Integer, FlatViewGroup> 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;
}
}

View File

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