Implement overflow:visible/hidden attribute (defaults to visible)

Summary:
Prior to this patch DrawCommands weren't get clipped by parent DrawCommands at all. For example, a <View> element with size 200x200 with overflow:hidden should clip its child which is 400x400, but this didn't happen. However, if parent <View> would mount to an Android View, it would clip the child regardless of the overflow attribute value (because Android Views always clip whatever is drawing inside that View against its boundaries).

This diff is fixing these issue, implementing overflow attribute support and making clipping behavior consistent between nodes that mount to View and nodes that don't.

Reviewed By: ahmedre

Differential Revision: D2768643
This commit is contained in:
Denis Koroskin 2015-12-20 20:34:36 -08:00 коммит произвёл Ahmed El-Helw
Родитель f738b598d8
Коммит 6f06f76e38
11 изменённых файлов: 249 добавлений и 35 удалений

Просмотреть файл

@ -9,6 +9,8 @@
package com.facebook.react.flat; package com.facebook.react.flat;
import android.graphics.Canvas;
/** /**
* Base class for all DrawCommands. Becomes immutable once it has its bounds set. Until then, a * Base class for all DrawCommands. Becomes immutable once it has its bounds set. Until then, a
* subclass is able to mutate any of its properties (e.g. updating Layout in DrawTextLayout). * subclass is able to mutate any of its properties (e.g. updating Layout in DrawTextLayout).
@ -22,8 +24,24 @@ package com.facebook.react.flat;
private float mTop; private float mTop;
private float mRight; private float mRight;
private float mBottom; private float mBottom;
private float mClipLeft;
private float mClipTop;
private float mClipRight;
private float mClipBottom;
private boolean mFrozen; private boolean mFrozen;
@Override
public final void draw(FlatViewGroup parent, Canvas canvas) {
if (shouldClip()) {
canvas.save();
canvas.clipRect(mClipLeft, mClipTop, mClipRight, mClipBottom);
onDraw(canvas);
canvas.restore();
} else {
onDraw(canvas);
}
}
/** /**
* Updates boundaries of the AbstractDrawCommand and freezes it. * Updates boundaries of the AbstractDrawCommand and freezes it.
* Will return a frozen copy if the current AbstractDrawCommand cannot be mutated. * Will return a frozen copy if the current AbstractDrawCommand cannot be mutated.
@ -32,16 +50,27 @@ package com.facebook.react.flat;
float left, float left,
float top, float top,
float right, float right,
float bottom) { float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
if (mFrozen) { if (mFrozen) {
// see if we can reuse it // see if we can reuse it
if (boundsMatch(left, top, right, bottom)) { boolean boundsMatch = boundsMatch(left, top, right, bottom);
boolean clipBoundsMatch = clipBoundsMatch(clipLeft, clipTop, clipRight, clipBottom);
if (boundsMatch && clipBoundsMatch) {
return this; return this;
} }
try { try {
AbstractDrawCommand copy = (AbstractDrawCommand) clone(); AbstractDrawCommand copy = (AbstractDrawCommand) clone();
if (!boundsMatch) {
copy.setBounds(left, top, right, bottom); copy.setBounds(left, top, right, bottom);
}
if (!clipBoundsMatch) {
copy.setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
}
return copy; return copy;
} catch (CloneNotSupportedException e) { } catch (CloneNotSupportedException e) {
// This should not happen since AbstractDrawCommand implements Cloneable // This should not happen since AbstractDrawCommand implements Cloneable
@ -50,6 +79,7 @@ package com.facebook.react.flat;
} }
setBounds(left, top, right, bottom); setBounds(left, top, right, bottom);
setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
mFrozen = true; mFrozen = true;
return this; return this;
} }
@ -103,6 +133,12 @@ package com.facebook.react.flat;
return mBottom; return mBottom;
} }
protected abstract void onDraw(Canvas canvas);
protected boolean shouldClip() {
return mLeft != mClipLeft || mTop != mClipTop || mRight != mClipRight || mBottom != mClipBottom;
}
protected void onBoundsChanged() { protected void onBoundsChanged() {
} }
@ -118,10 +154,26 @@ package com.facebook.react.flat;
onBoundsChanged(); onBoundsChanged();
} }
private void setClipBounds(float clipLeft, float clipTop, float clipRight, float clipBottom) {
mClipLeft = clipLeft;
mClipTop = clipTop;
mClipRight = clipRight;
mClipBottom = clipBottom;
}
/** /**
* Returns true if boundaries match and don't need to be updated. False otherwise. * Returns true if boundaries match and don't need to be updated. False otherwise.
*/ */
private boolean boundsMatch(float left, float top, float right, float bottom) { private boolean boundsMatch(float left, float top, float right, float bottom) {
return mLeft == left && mTop == top && mRight == right && mBottom == bottom; return mLeft == left && mTop == top && mRight == right && mBottom == bottom;
} }
private boolean clipBoundsMatch(
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
return mClipLeft == clipLeft && mClipTop == clipTop &&
mClipRight == clipRight && mClipBottom == clipBottom;
}
} }

Просмотреть файл

@ -26,7 +26,7 @@ import android.graphics.Paint;
} }
@Override @Override
public void draw(FlatViewGroup parent, Canvas canvas) { public void onDraw(Canvas canvas) {
PAINT.setColor(mBackgroundColor); PAINT.setColor(mBackgroundColor);
canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), PAINT); canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), PAINT);
} }

Просмотреть файл

@ -169,7 +169,7 @@ import com.facebook.csslayout.Spacing;
} }
@Override @Override
public void draw(FlatViewGroup parent, Canvas canvas) { protected void onDraw(Canvas canvas) {
if (getBorderRadius() >= 0.5f) { if (getBorderRadius() >= 0.5f) {
drawRoundedBorders(canvas); drawRoundedBorders(canvas);
} else { } else {

Просмотреть файл

@ -80,7 +80,7 @@ import com.facebook.react.views.image.ImageResizeMode;
} }
@Override @Override
public void draw(FlatViewGroup parent, Canvas canvas) { protected void onDraw(Canvas canvas) {
Bitmap bitmap = Assertions.assumeNotNull(mBitmapRequestHelper).getBitmap(); Bitmap bitmap = Assertions.assumeNotNull(mBitmapRequestHelper).getBitmap();
if (bitmap == null) { if (bitmap == null) {
return; return;
@ -88,11 +88,6 @@ import com.facebook.react.views.image.ImageResizeMode;
PAINT.setColorFilter(mColorFilter); PAINT.setColorFilter(mColorFilter);
if (mForceClip) {
canvas.save();
canvas.clipRect(getLeft(), getTop(), getRight(), getBottom());
}
if (getBorderRadius() < 0.5f) { if (getBorderRadius() < 0.5f) {
canvas.drawBitmap(bitmap, mTransform, PAINT); canvas.drawBitmap(bitmap, mTransform, PAINT);
} else { } else {
@ -105,10 +100,11 @@ import com.facebook.react.views.image.ImageResizeMode;
} }
drawBorders(canvas); drawBorders(canvas);
if (mForceClip) {
canvas.restore();
} }
@Override
protected boolean shouldClip() {
return mForceClip || super.shouldClip();
} }
@Override @Override

Просмотреть файл

@ -35,7 +35,7 @@ import android.text.Layout;
} }
@Override @Override
public void draw(FlatViewGroup parent, Canvas canvas) { protected void onDraw(Canvas canvas) {
float left = getLeft(); float left = getLeft();
float top = getTop(); float top = getTop();

Просмотреть файл

@ -47,6 +47,7 @@ import com.facebook.react.uimanager.ViewProps;
private boolean mBackingViewIsCreated; private boolean mBackingViewIsCreated;
private @Nullable DrawBackgroundColor mDrawBackground; private @Nullable DrawBackgroundColor mDrawBackground;
private int mMoveToIndexInParent; private int mMoveToIndexInParent;
private boolean mIsOverflowVisible = true;
/* package */ void handleUpdateProperties(CatalystStylesDiffMap styles) { /* package */ void handleUpdateProperties(CatalystStylesDiffMap styles) {
if (!mountsToView()) { if (!mountsToView()) {
@ -72,13 +73,21 @@ import com.facebook.react.uimanager.ViewProps;
float left, float left,
float top, float top,
float right, float right,
float bottom) { float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
if (mDrawBackground != null) { if (mDrawBackground != null) {
mDrawBackground = (DrawBackgroundColor) mDrawBackground.updateBoundsAndFreeze( mDrawBackground = (DrawBackgroundColor) mDrawBackground.updateBoundsAndFreeze(
left, left,
top, top,
right, right,
bottom); bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
stateBuilder.addDrawCommand(mDrawBackground); stateBuilder.addDrawCommand(mDrawBackground);
} }
} }
@ -89,6 +98,16 @@ import com.facebook.react.uimanager.ViewProps;
invalidate(); invalidate();
} }
@ReactProp(name = "overflow")
public void setOverflow(String overflow) {
mIsOverflowVisible = "visible".equals(overflow);
invalidate();
}
public final boolean isOverflowVisible() {
return mIsOverflowVisible;
}
@Override @Override
public final int getScreenX() { public final int getScreenX() {
return mViewLeft; return mViewLeft;

Просмотреть файл

@ -62,6 +62,7 @@ import com.facebook.react.uimanager.ReactCompoundView;
/* package */ FlatViewGroup(Context context) { /* package */ FlatViewGroup(Context context) {
super(context); super(context);
setClipChildren(false);
} }
@Override @Override

Просмотреть файл

@ -54,15 +54,32 @@ import com.facebook.react.views.image.ImageResizeMode;
float left, float left,
float top, float top,
float right, float right,
float bottom) { float bottom,
super.collectState(stateBuilder, left, top, right, bottom); float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
super.collectState(
stateBuilder,
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
if (mDrawImage.hasImageRequest()) { if (mDrawImage.hasImageRequest()) {
mDrawImage = (T) mDrawImage.updateBoundsAndFreeze( mDrawImage = (T) mDrawImage.updateBoundsAndFreeze(
left, left,
top, top,
right, right,
bottom); bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
stateBuilder.addDrawCommand(mDrawImage); stateBuilder.addDrawCommand(mDrawImage);
stateBuilder.addAttachDetachListener(mDrawImage); stateBuilder.addAttachDetachListener(mDrawImage);
} }

Просмотреть файл

@ -137,8 +137,22 @@ import com.facebook.react.uimanager.ViewProps;
float left, float left,
float top, float top,
float right, float right,
float bottom) { float bottom,
super.collectState(stateBuilder, left, top, right, bottom); float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
super.collectState(
stateBuilder,
left,
top,
right,
bottom,
clipLeft,
clipRight,
clipTop,
clipBottom);
if (mText == null) { if (mText == null) {
// nothing to draw (empty text). // nothing to draw (empty text).
@ -158,7 +172,15 @@ import com.facebook.react.uimanager.ViewProps;
INCLUDE_PADDING)); INCLUDE_PADDING));
} }
mDrawCommand = (DrawTextLayout) mDrawCommand.updateBoundsAndFreeze(left, top, right, bottom); mDrawCommand = (DrawTextLayout) mDrawCommand.updateBoundsAndFreeze(
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
stateBuilder.addDrawCommand(mDrawCommand); stateBuilder.addDrawCommand(mDrawCommand);
} }

Просмотреть файл

@ -27,11 +27,32 @@ import com.facebook.react.uimanager.ViewProps;
float left, float left,
float top, float top,
float right, float right,
float bottom) { float bottom,
super.collectState(stateBuilder, left, top, right, bottom); float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
super.collectState(
stateBuilder,
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
if (mDrawBorder != null) { if (mDrawBorder != null) {
mDrawBorder = (DrawBorder) mDrawBorder.updateBoundsAndFreeze(left, top, right, bottom); mDrawBorder = (DrawBorder) mDrawBorder.updateBoundsAndFreeze(
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom);
stateBuilder.addDrawCommand(mDrawBorder); stateBuilder.addDrawCommand(mDrawBorder);
} }
} }

Просмотреть файл

@ -61,12 +61,23 @@ import com.facebook.react.uimanager.events.EventDispatcher;
float width = node.getLayoutWidth(); float width = node.getLayoutWidth();
float height = node.getLayoutHeight(); float height = node.getLayoutHeight();
collectStateForMountableNode(node, tag, width, height);
float left = node.getLayoutX(); float left = node.getLayoutX();
float top = node.getLayoutY(); float top = node.getLayoutY();
float right = left + width; float right = left + width;
float bottom = top + height; float bottom = top + height;
collectStateForMountableNode(
node,
tag,
left,
top,
right,
bottom,
Float.NEGATIVE_INFINITY,
Float.NEGATIVE_INFINITY,
Float.POSITIVE_INFINITY,
Float.POSITIVE_INFINITY);
node.updateNodeRegion(left, top, right, bottom); node.updateNodeRegion(left, top, right, bottom);
mViewsToUpdateBounds.add(node); mViewsToUpdateBounds.add(node);
@ -166,8 +177,14 @@ import com.facebook.react.uimanager.events.EventDispatcher;
private void collectStateForMountableNode( private void collectStateForMountableNode(
FlatShadowNode node, FlatShadowNode node,
int tag, int tag,
float width, float left,
float height) { float top,
float right,
float bottom,
float clipLeft,
float clipTop,
float clipRight,
float clipBottom) {
mDrawCommands.start(node.getDrawCommands()); mDrawCommands.start(node.getDrawCommands());
mAttachDetachListeners.start(node.getAttachDetachListeners()); mAttachDetachListeners.start(node.getAttachDetachListeners());
mNodeRegions.start(node.getNodeRegions()); mNodeRegions.start(node.getNodeRegions());
@ -181,6 +198,14 @@ import com.facebook.react.uimanager.events.EventDispatcher;
isAndroidView = true; isAndroidView = true;
needsCustomLayoutForChildren = androidView.needsCustomLayoutForChildren(); needsCustomLayoutForChildren = androidView.needsCustomLayoutForChildren();
// AndroidView might scroll (e.g. ScrollView) so we need to reset clip bounds here
// Otherwise, we might scroll clipped content. If AndroidView doesn't scroll, this is still
// harmless, because AndroidView will do its own clipping anyway.
clipLeft = Float.NEGATIVE_INFINITY;
clipTop = Float.NEGATIVE_INFINITY;
clipRight = Float.POSITIVE_INFINITY;
clipBottom = Float.POSITIVE_INFINITY;
} else if (node.isVirtualAnchor()) { } else if (node.isVirtualAnchor()) {
// If RCTText is mounted to View, virtual children will not receive any touch events // If RCTText is mounted to View, virtual children will not receive any touch events
// because they don't get added to nodeRegions, so nodeRegions will be empty and // because they don't get added to nodeRegions, so nodeRegions will be empty and
@ -190,7 +215,18 @@ import com.facebook.react.uimanager.events.EventDispatcher;
addNodeRegion(node.getNodeRegion()); addNodeRegion(node.getNodeRegion());
} }
collectStateRecursively(node, 0, 0, width, height, isAndroidView, needsCustomLayoutForChildren); collectStateRecursively(
node,
left,
top,
right,
bottom,
clipLeft,
clipTop,
clipRight,
clipBottom,
isAndroidView,
needsCustomLayoutForChildren);
boolean shouldUpdateMountState = false; boolean shouldUpdateMountState = false;
final DrawCommand[] drawCommands = mDrawCommands.finish(); final DrawCommand[] drawCommands = mDrawCommands.finish();
@ -290,6 +326,10 @@ import com.facebook.react.uimanager.events.EventDispatcher;
float top, float top,
float right, float right,
float bottom, float bottom,
float parentClipLeft,
float parentClipTop,
float parentClipRight,
float parentClipBottom,
boolean isAndroidView, boolean isAndroidView,
boolean needsCustomLayoutForChildren) { boolean needsCustomLayoutForChildren) {
if (node.hasNewLayout()) { if (node.hasNewLayout()) {
@ -307,7 +347,19 @@ import com.facebook.react.uimanager.events.EventDispatcher;
Math.round(bottom - top))); Math.round(bottom - top)));
} }
node.collectState(this, left, top, right, bottom); float clipLeft = Math.max(left, parentClipLeft);
float clipTop = Math.max(top, parentClipTop);
float clipRight = Math.min(right, parentClipRight);
float clipBottom = Math.min(bottom, parentClipBottom);
node.collectState(this, left, top, right, bottom, clipLeft, clipTop, clipRight, clipBottom);
if (node.isOverflowVisible()) {
clipLeft = parentClipLeft;
clipTop = parentClipTop;
clipRight = parentClipRight;
clipBottom = parentClipBottom;
}
for (int i = 0, childCount = node.getChildCount(); i != childCount; ++i) { for (int i = 0, childCount = node.getChildCount(); i != childCount; ++i) {
FlatShadowNode child = (FlatShadowNode) node.getChildAt(i); FlatShadowNode child = (FlatShadowNode) node.getChildAt(i);
@ -316,7 +368,16 @@ import com.facebook.react.uimanager.events.EventDispatcher;
continue; continue;
} }
processNodeAndCollectState(child, left, top, isAndroidView, needsCustomLayoutForChildren); processNodeAndCollectState(
child,
left,
top,
clipLeft,
clipTop,
clipRight,
clipBottom,
isAndroidView,
needsCustomLayoutForChildren);
} }
} }
@ -337,6 +398,10 @@ import com.facebook.react.uimanager.events.EventDispatcher;
FlatShadowNode node, FlatShadowNode node,
float parentLeft, float parentLeft,
float parentTop, float parentTop,
float parentClipLeft,
float parentClipTop,
float parentClipRight,
float parentClipBottom,
boolean parentIsAndroidView, boolean parentIsAndroidView,
boolean needsCustomLayout) { boolean needsCustomLayout) {
int tag = node.getReactTag(); int tag = node.getReactTag();
@ -359,13 +424,34 @@ import com.facebook.react.uimanager.events.EventDispatcher;
mDrawCommands.add(DrawView.INSTANCE); mDrawCommands.add(DrawView.INSTANCE);
} }
collectStateForMountableNode(node, tag, width, height); collectStateForMountableNode(
node,
tag,
left - left,
top - top,
right - left,
bottom - top,
parentClipLeft - left,
parentClipTop - top,
parentClipRight - left,
parentClipBottom - top);
if (!needsCustomLayout) { if (!needsCustomLayout) {
mViewsToUpdateBounds.add(node); mViewsToUpdateBounds.add(node);
} }
} else { } else {
collectStateRecursively(node, left, top, right, bottom, false, false); collectStateRecursively(
node,
left,
top,
right,
bottom,
parentClipLeft,
parentClipTop,
parentClipRight,
parentClipBottom,
false,
false);
addNodeRegion(node.getNodeRegion()); addNodeRegion(node.getNodeRegion());
} }
} }