From 2d8cbd70bc5c707cfa57153ce12f8633a76e90e0 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 28 Jul 2016 14:45:22 -0700 Subject: [PATCH] Support rounded clipping Summary: Support rounded clipping in Nodes. Before, if a view had a radius and had overflow of hidden, its children could still draw outside of it (specifically, in the area between the rounded rect and square rect) - this is due to the fact that clipping is, by default, rectangular. This patch supports this type of rounded clipping. Differential Revision: D3634861 --- .../react/flat/AbstractDrawCommand.java | 2 +- .../com/facebook/react/flat/DrawView.java | 86 ++++++++++++++++++- .../facebook/react/flat/FlatShadowNode.java | 16 +++- .../java/com/facebook/react/flat/RCTView.java | 6 ++ 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java index e803b8d34..b1d671b75 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java @@ -75,7 +75,7 @@ import android.graphics.Color; return mClipBottom; } - protected final void applyClipping(Canvas canvas) { + protected void applyClipping(Canvas canvas) { canvas.clipRect(mClipLeft, mClipTop, mClipRight, mClipBottom); } 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 ec88ca215..2775cf5ce 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawView.java @@ -9,9 +9,16 @@ package com.facebook.react.flat; +import javax.annotation.Nullable; + import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; /* package */ final class DrawView extends AbstractDrawCommand { + // the minimum rounded clipping value before we actually do rounded clipping + /* package */ static final float MINIMUM_ROUNDED_CLIPPING_VALUE = 0.5f; + private final RectF TMP_RECT = new RectF(); /* package */ final int reactTag; // Indicates whether this DrawView has been previously mounted to a clipping FlatViewGroup. This @@ -22,6 +29,13 @@ import android.graphics.Canvas; // quickest way to create unreproducible super bugs. /* package */ boolean mWasMounted; + // the clipping radius - if this is greater than MINIMUM_ROUNDED_CLIPPING_VALUE, we clip using + // a rounded path, otherwise we clip in a rectangular fashion. + private float mClipRadius; + + // the path to clip against if we're doing path clipping for rounded borders. + @Nullable private Path mPath; + public DrawView(int reactTag) { this.reactTag = reactTag; } @@ -41,10 +55,39 @@ import android.graphics.Canvas; float clipLeft, float clipTop, float clipRight, - float clipBottom) { + float clipBottom, + float clipRadius) { DrawView drawView = (DrawView) - updateBoundsAndFreeze(left, top, right, bottom, clipLeft, clipTop, clipRight, clipBottom); + updateBoundsAndFreeze( + left, + top, + right, + bottom, + clipLeft, + clipTop, + clipRight, + clipBottom); + boolean clipRadiusChanged = Math.abs(mClipRadius - clipRadius) > 0.001f; + if (clipRadiusChanged && drawView == this) { + // 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 { + drawView = (DrawView) clone(); + } catch (CloneNotSupportedException e) { + // This should not happen since AbstractDrawCommand implements Cloneable + throw new RuntimeException(e); + } + } + if (drawView != this) { + drawView.mClipRadius = clipRadius; + if (clipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) { + // update the path that we'll clip based on + updateClipPath(drawView); + } else { + drawView.mPath = null; + } + // 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. @@ -56,7 +99,7 @@ import android.graphics.Canvas; @Override public void draw(FlatViewGroup parent, Canvas canvas) { onPreDraw(parent, canvas); - if (mNeedsClipping) { + if (mNeedsClipping || mClipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) { canvas.save(Canvas.CLIP_SAVE_FLAG); applyClipping(canvas); parent.drawNextChild(canvas); @@ -66,6 +109,43 @@ import android.graphics.Canvas; } } + /** + * Update the path with which we'll clip this view + * @param drawView the drawView to update the path for + */ + private void updateClipPath(DrawView drawView) { + // make or reset the path + if (drawView.mPath == null) { + drawView.mPath = new Path(); + } else { + drawView.mPath.reset(); + } + + TMP_RECT.set( + getLeft(), + getTop(), + getRight(), + getBottom()); + + // set the path + drawView.mPath.addRoundRect( + TMP_RECT, + drawView.mClipRadius, + drawView.mClipRadius, + Path.Direction.CW); + } + + @Override + protected void applyClipping(Canvas canvas) { + // only clip using a path if our radius is greater than some minimum threshold, because + // clipPath is more expensive than clipRect. + if (mClipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) { + canvas.clipPath(mPath); + } else { + super.applyClipping(canvas); + } + } + @Override protected void onDraw(Canvas canvas) { // no op as we override draw. 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 09a1641ff..9c5aae563 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatShadowNode.java @@ -60,7 +60,6 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; private boolean mBackingViewIsCreated; private @Nullable DrawView mDrawView; private @Nullable DrawBackgroundColor mDrawBackground; - private boolean mClipToBounds = false; private boolean mIsUpdated = true; private boolean mForceMountChildrenToView; private float mClipLeft; @@ -82,6 +81,10 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; private int mLayoutWidth; private int mLayoutHeight; + // clip radius + float mClipRadius; + boolean mClipToBounds = false; + /* package */ void handleUpdateProperties(ReactStylesDiffMap styles) { if (!mountsToView()) { // Make sure we mount this FlatShadowNode to a View if any of these properties are present. @@ -152,6 +155,11 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; mClipToBounds = "hidden".equals(overflow); if (mClipToBounds) { mOverflowsContainer = false; + if (mClipRadius > DrawView.MINIMUM_ROUNDED_CLIPPING_VALUE) { + // mount to a view if we are overflow: hidden and are clipping, so that we can do one + // clipPath to clip all the children of this node (both DrawCommands and Views). + forceMountToView(); + } } else { updateOverflowsContainer(); } @@ -483,6 +491,9 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; // DrawView anyway, as reactTag is final, and our DrawView instance is the static copy. mDrawView = new DrawView(getReactTag()); } + + // avoid path clipping if overflow: visible + float clipRadius = mClipToBounds ? mClipRadius : 0.0f; // We have the correct react tag, but we may need a new copy with updated bounds. If the bounds // match or were never set, the same view is returned. mDrawView = mDrawView.collectDrawView( @@ -493,7 +504,8 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; clipLeft, clipTop, clipRight, - clipBottom); + clipBottom, + clipRadius); return mDrawView; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java index 9cef2ef41..97e6f8a16 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTView.java @@ -91,6 +91,12 @@ import com.facebook.react.uimanager.annotations.ReactPropGroup; @ReactProp(name = "borderRadius") public void setBorderRadius(float borderRadius) { + mClipRadius = borderRadius; + if (mClipToBounds && borderRadius > DrawView.MINIMUM_ROUNDED_CLIPPING_VALUE) { + // mount to a view if we are overflow: hidden and are clipping, so that we can do one + // clipPath to clip all the children of this node (both DrawCommands and Views). + forceMountToView(); + } getMutableBorder().setBorderRadius(PixelUtil.toPixelFromDIP(borderRadius)); }