From 4994d6a389b4e41ba25e802edab5d3fdc9e8a4f1 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Wed, 18 Oct 2017 19:29:31 -0700 Subject: [PATCH] Implement partial rounded borders Reviewed By: achen1 Differential Revision: D5982241 fbshipit-source-id: 2f694daca7e1b16b5ff65f07c7d15dd558a4b7e8 --- .../view/ReactViewBackgroundDrawable.java | 140 ++++++++++++++---- .../react/views/view/ReactViewGroup.java | 41 +++-- 2 files changed, 137 insertions(+), 44 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java index 7a43ef9fc..16ebeccd8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java @@ -19,6 +19,7 @@ import android.graphics.Path; import android.graphics.PathEffect; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Build; import com.facebook.react.common.annotations.VisibleForTesting; @@ -30,15 +31,15 @@ import java.util.Locale; import javax.annotation.Nullable; /** - * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports - * drawing background color and borders (including rounded borders) by providing a react friendly - * API (setter for each of those properties). + * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports drawing + * background color and borders (including rounded borders) by providing a react friendly API + * (setter for each of those properties). * - * The implementation tries to allocate as few objects as possible depending on which properties are - * set. E.g. for views with rounded background/borders we allocate {@code mPathForBorderRadius} and - * {@code mTempRectForBorderRadius}. In case when view have a rectangular borders we allocate - * {@code mBorderWidthResult} and similar. When only background color is set we won't allocate any - * extra/unnecessary objects. + *

The implementation tries to allocate as few objects as possible depending on which properties + * are set. E.g. for views with rounded background/borders we allocate {@code + * mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view + * have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only + * background color is set we won't allocate any extra/unnecessary objects. */ public class ReactViewBackgroundDrawable extends Drawable { @@ -83,10 +84,12 @@ public class ReactViewBackgroundDrawable extends Drawable { /* Used for rounded border and rounded background */ private @Nullable PathEffect mPathEffectForBorderStyle; - private @Nullable Path mPathForBorderRadius; + private @Nullable Path mInnerClipPathForBorderRadius; + private @Nullable Path mOuterClipPathForBorderRadius; private @Nullable Path mPathForBorderRadiusOutline; private @Nullable Path mPathForBorder; - private @Nullable RectF mTempRectForBorderRadius; + private @Nullable RectF mInnerClipTempRectForBorderRadius; + private @Nullable RectF mOuterClipTempRectForBorderRadius; private @Nullable RectF mTempRectForBorderRadiusOutline; private boolean mNeedUpdatePathForBorderRadius = false; private float mBorderRadius = YogaConstants.UNDEFINED; @@ -169,8 +172,13 @@ public class ReactViewBackgroundDrawable extends Drawable { } if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { mBorderWidth.set(position, width); - if (position == Spacing.ALL) { - mNeedUpdatePathForBorderRadius = true; + switch (position) { + case Spacing.ALL: + case Spacing.LEFT: + case Spacing.BOTTOM: + case Spacing.RIGHT: + case Spacing.TOP: + mNeedUpdatePathForBorderRadius = true; } invalidateSelf(); } @@ -266,44 +274,87 @@ public class ReactViewBackgroundDrawable extends Drawable { private void drawRoundedBackgroundWithBorders(Canvas canvas) { updatePath(); + canvas.save(); + int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); if (Color.alpha(useColor) != 0) { // color is not transparent mPaint.setColor(useColor); mPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(mPathForBorderRadius, mPaint); + canvas.drawPath(mInnerClipPathForBorderRadius, mPaint); } - // maybe draw borders? - float fullBorderWidth = getFullBorderWidth(); - if (fullBorderWidth > 0) { + + final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); + final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); + final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); + final float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); + final float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); + + if (borderTopWidth > 0 + || borderBottomWidth > 0 + || borderLeftWidth > 0 + || borderRightWidth > 0) { int borderColor = getFullBorderColor(); mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha)); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(fullBorderWidth); - canvas.drawPath(mPathForBorderRadius, mPaint); + mPaint.setStyle(Paint.Style.FILL); + + // Draw border + canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); + canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); + canvas.drawRect(getBounds(), mPaint); } + + canvas.restore(); } private void updatePath() { if (!mNeedUpdatePathForBorderRadius) { return; } + mNeedUpdatePathForBorderRadius = false; - if (mPathForBorderRadius == null) { - mPathForBorderRadius = new Path(); - mTempRectForBorderRadius = new RectF(); + + if (mInnerClipPathForBorderRadius == null) { + mInnerClipPathForBorderRadius = new Path(); + } + + if (mOuterClipPathForBorderRadius == null) { + mOuterClipPathForBorderRadius = new Path(); + } + + if (mPathForBorderRadiusOutline == null) { mPathForBorderRadiusOutline = new Path(); + } + + if (mInnerClipTempRectForBorderRadius == null) { + mInnerClipTempRectForBorderRadius = new RectF(); + } + + if (mOuterClipTempRectForBorderRadius == null) { + mOuterClipTempRectForBorderRadius = new RectF(); + } + + if (mTempRectForBorderRadiusOutline == null) { mTempRectForBorderRadiusOutline = new RectF(); } - mPathForBorderRadius.reset(); + mInnerClipPathForBorderRadius.reset(); + mOuterClipPathForBorderRadius.reset(); mPathForBorderRadiusOutline.reset(); - mTempRectForBorderRadius.set(getBounds()); + mInnerClipTempRectForBorderRadius.set(getBounds()); + mOuterClipTempRectForBorderRadius.set(getBounds()); mTempRectForBorderRadiusOutline.set(getBounds()); - float fullBorderWidth = getFullBorderWidth(); - if (fullBorderWidth > 0) { - mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f); - } + + final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); + final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); + final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); + final float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); + final float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); + + mInnerClipTempRectForBorderRadius.top += borderTopWidth; + mInnerClipTempRectForBorderRadius.bottom -= borderBottomWidth; + mInnerClipTempRectForBorderRadius.left += borderLeftWidth; + mInnerClipTempRectForBorderRadius.right -= borderRightWidth; final float borderRadius = getFullBorderRadius(); final float topLeftRadius = @@ -315,8 +366,22 @@ public class ReactViewBackgroundDrawable extends Drawable { final float bottomRightRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT); - mPathForBorderRadius.addRoundRect( - mTempRectForBorderRadius, + mInnerClipPathForBorderRadius.addRoundRect( + mInnerClipTempRectForBorderRadius, + new float[] { + Math.max(topLeftRadius - borderLeftWidth, 0), + Math.max(topLeftRadius - borderTopWidth, 0), + Math.max(topRightRadius - borderRightWidth, 0), + Math.max(topRightRadius - borderTopWidth, 0), + Math.max(bottomRightRadius - borderRightWidth, 0), + Math.max(bottomRightRadius - borderBottomWidth, 0), + Math.max(bottomLeftRadius - borderLeftWidth, 0), + Math.max(bottomLeftRadius - borderBottomWidth, 0), + }, + Path.Direction.CW); + + mOuterClipPathForBorderRadius.addRoundRect( + mOuterClipTempRectForBorderRadius, new float[] { topLeftRadius, topLeftRadius, @@ -329,6 +394,7 @@ public class ReactViewBackgroundDrawable extends Drawable { }, Path.Direction.CW); + float extraRadiusForOutline = 0; if (mBorderWidth != null) { @@ -350,6 +416,20 @@ public class ReactViewBackgroundDrawable extends Drawable { Path.Direction.CW); } + public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { + if (mBorderWidth == null) { + return defaultValue; + } + + final float width = mBorderWidth.getRaw(spacingType); + + if (YogaConstants.isUndefined(width)) { + return defaultValue; + } + + return width; + } + /** * Set type of border */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 4de2ce855..399a13e3f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -33,6 +33,7 @@ import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ReactPointerEventsView; import com.facebook.react.uimanager.ReactZIndexedViewGroup; +import com.facebook.react.uimanager.Spacing; import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper; import javax.annotation.Nullable; @@ -624,13 +625,25 @@ public class ReactViewGroup extends ViewGroup implements float top = 0f; float right = getWidth(); float bottom = getHeight(); - final float borderWidth = mReactBackgroundDrawable.getFullBorderWidth(); - if (borderWidth != 0f) { - left += borderWidth; - top += borderWidth; - right -= borderWidth; - bottom -= borderWidth; + final float borderWidth = mReactBackgroundDrawable.getFullBorderWidth(); + final float borderTopWidth = + mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); + final float borderBottomWidth = + mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); + final float borderLeftWidth = + mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); + final float borderRightWidth = + mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); + + if (borderTopWidth > 0 + || borderLeftWidth > 0 + || borderBottomWidth > 0 + || borderRightWidth > 0) { + left += borderLeftWidth; + top += borderTopWidth; + right -= borderRightWidth; + bottom -= borderBottomWidth; } final float borderRadius = mReactBackgroundDrawable.getFullBorderRadius(); @@ -659,14 +672,14 @@ public class ReactViewGroup extends ViewGroup implements mPath.addRoundRect( new RectF(left, top, right, bottom), new float[] { - Math.max(topLeftBorderRadius - borderWidth, 0), - Math.max(topLeftBorderRadius - borderWidth, 0), - Math.max(topRightBorderRadius - borderWidth, 0), - Math.max(topRightBorderRadius - borderWidth, 0), - Math.max(bottomRightBorderRadius - borderWidth, 0), - Math.max(bottomRightBorderRadius - borderWidth, 0), - Math.max(bottomLeftBorderRadius - borderWidth, 0), - Math.max(bottomLeftBorderRadius - borderWidth, 0), + Math.max(topLeftBorderRadius - borderLeftWidth, 0), + Math.max(topLeftBorderRadius - borderTopWidth, 0), + Math.max(topRightBorderRadius - borderRightWidth, 0), + Math.max(topRightBorderRadius - borderTopWidth, 0), + Math.max(bottomRightBorderRadius - borderRightWidth, 0), + Math.max(bottomRightBorderRadius - borderBottomWidth, 0), + Math.max(bottomLeftBorderRadius - borderLeftWidth, 0), + Math.max(bottomLeftBorderRadius - borderBottomWidth, 0), }, Path.Direction.CW); canvas.clipPath(mPath);