Add directional clipping command manager.

Summary: Add directional aware clipping to DrawCommandManager.  Currently not attached to FlatViewGroup logic, with the plan to keep this unattached until we are clipping the way we want to in the final state.

Reviewed By: ahmedre

Differential Revision: D3622253
This commit is contained in:
Seth Kirby 2016-08-01 12:26:50 -07:00 committed by Ahmed El-Helw
parent f850e61fdb
commit e96f6fa585
10 changed files with 391 additions and 43 deletions

View File

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

View File

@ -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<Integer, DrawView> 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<Integer, View> 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<View> 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);
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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