From 29caed22cc1c9a426e524e498cf44cf18df0577e Mon Sep 17 00:00:00 2001 From: Sharon Zheng Date: Thu, 10 Nov 2022 19:09:51 -0800 Subject: [PATCH] rounded Views with borders shows small gap Summary: When a __rounded__ View on Android has a border, a small gap appears between the border and the center of the view (most noticeably when the background and border colors are the same) Since the border is drawn on top of the other layers, the approach here is to make the center of the View slightly larger so that there is an overlap with the border, and closing the visible gap There are 2 cases for a View with a border: 1. `borderWidth` is set for a consistent border width around all 4 edges 2. Uneven border widths are set (using `borderTopWidth`, `borderLeftWidth, ...) **How is a rounded rectangle drawn?** __Case 1__: `borderWidth` is set for a consistent border width around all 4 edges - Before: - first, `mInnerClipPathForBorderRadius` was used to draw the center of the View - then the border is drawn along the path of `mCenterDraw` with a stroke width of the border width - Now: - `mBackgroundColorRenderPath` is used to draw the center of the View and is exactly a slightly enlarged version of `mInnerClipPathForBorderRadius` __Case 2__: Uneven border widths are set (using borderTopWidth, borderLeftWidth, ...) - Before: - `mInnerClipPathForBorderRadius` was used to draw the center of the View - for each edge, a quadrilateral is drawn - `mOuterClipPathForBorderRadius` clips the outer edge of the border - `mInnerClipPathForBorderRadius` (same is used to draw the center of the View) clips the inner edge of the border - Now: - `mBackgroundColorRenderPath` is used to draw the center of the View, allowing `mInnerClipPathForBorderRadius` to persist as the path that clips the inner edge of the border When `mGapBetweenPaths` = 0, `mBackgroundColorRenderPath` == `mInnerClipPathForBorderRadius`, which is exactly the original implementation Changelog: [Internal][Fixed] - rounded Views with borders shows small gap Reviewed By: mdvacca Differential Revision: D39979567 fbshipit-source-id: 6db71d14ead6256e1b7becf73862e0a537c6a47b --- .../react/config/ReactFeatureFlags.java | 6 ++ .../view/ReactViewBackgroundDrawable.java | 78 ++++++++++++++----- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 42492c19ff6..0b5c2402428 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -126,4 +126,10 @@ public class ReactFeatureFlags { // TODO (T136375139): Remove this once finish testing public static boolean enableAtomicRegisterSegment = false; + + /** + * Allow closing the small gap that appears between paths when drawing a rounded View with a + * border. + */ + public static boolean enableCloseVisibleGapBetweenPaths = true; } 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 827aaec706e..532d025c1aa 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 @@ -27,6 +27,7 @@ import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.Nullable; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.modules.i18nmanager.I18nUtil; import com.facebook.react.uimanager.FloatUtil; import com.facebook.react.uimanager.Spacing; @@ -86,6 +87,7 @@ public class ReactViewBackgroundDrawable extends Drawable { private @Nullable BorderStyle mBorderStyle; private @Nullable Path mInnerClipPathForBorderRadius; + private @Nullable Path mBackgroundColorRenderPath; private @Nullable Path mOuterClipPathForBorderRadius; private @Nullable Path mPathForBorderRadiusOutline; private @Nullable Path mPathForBorder; @@ -107,6 +109,13 @@ public class ReactViewBackgroundDrawable extends Drawable { private int mColor = Color.TRANSPARENT; private int mAlpha = 255; + // There is a small gap between the edges of adjacent paths + // such as between the mBackgroundColorRenderPath and its border. + // The smallest amount (found to be 0.8f) is used to extend + // the paths, overlapping them and closing the visible gap. + private final float mGapBetweenPaths = + ReactFeatureFlags.enableCloseVisibleGapBetweenPaths ? 0.8f : 0.0f; + private @Nullable float[] mBorderCornerRadii; private final Context mContext; private int mLayoutDirection; @@ -329,11 +338,15 @@ public class ReactViewBackgroundDrawable extends Drawable { updatePath(); canvas.save(); + // Clip outer border + canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); + + // Draws the View without its border first (with background color fill) 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(mInnerClipPathForBorderRadius, mPaint); + canvas.drawPath(mBackgroundColorRenderPath, mPaint); } final RectF borderWidth = getDirectionAwareBorderInsets(); @@ -369,8 +382,7 @@ public class ReactViewBackgroundDrawable extends Drawable { else { mPaint.setStyle(Paint.Style.FILL); - // Draw border - canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); + // Clip inner border canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; @@ -416,27 +428,29 @@ public class ReactViewBackgroundDrawable extends Drawable { final float top = mOuterClipTempRectForBorderRadius.top; final float bottom = mOuterClipTempRectForBorderRadius.bottom; + // mGapBetweenPaths is used to close the gap between the diagonal + // edges of the quadrilaterals on adjacent sides of the rectangle if (borderWidth.left > 0) { final float x1 = left; - final float y1 = top; + final float y1 = top - mGapBetweenPaths; final float x2 = mInnerTopLeftCorner.x; - final float y2 = mInnerTopLeftCorner.y; + final float y2 = mInnerTopLeftCorner.y - mGapBetweenPaths; final float x3 = mInnerBottomLeftCorner.x; - final float y3 = mInnerBottomLeftCorner.y; + final float y3 = mInnerBottomLeftCorner.y + mGapBetweenPaths; final float x4 = left; - final float y4 = bottom; + final float y4 = bottom + mGapBetweenPaths; drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); } if (borderWidth.top > 0) { - final float x1 = left; + final float x1 = left - mGapBetweenPaths; final float y1 = top; - final float x2 = mInnerTopLeftCorner.x; + final float x2 = mInnerTopLeftCorner.x - mGapBetweenPaths; final float y2 = mInnerTopLeftCorner.y; - final float x3 = mInnerTopRightCorner.x; + final float x3 = mInnerTopRightCorner.x + mGapBetweenPaths; final float y3 = mInnerTopRightCorner.y; - final float x4 = right; + final float x4 = right + mGapBetweenPaths; final float y4 = top; drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); @@ -444,25 +458,25 @@ public class ReactViewBackgroundDrawable extends Drawable { if (borderWidth.right > 0) { final float x1 = right; - final float y1 = top; + final float y1 = top - mGapBetweenPaths; final float x2 = mInnerTopRightCorner.x; - final float y2 = mInnerTopRightCorner.y; + final float y2 = mInnerTopRightCorner.y - mGapBetweenPaths; final float x3 = mInnerBottomRightCorner.x; - final float y3 = mInnerBottomRightCorner.y; + final float y3 = mInnerBottomRightCorner.y + mGapBetweenPaths; final float x4 = right; - final float y4 = bottom; + final float y4 = bottom + mGapBetweenPaths; drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); } if (borderWidth.bottom > 0) { - final float x1 = left; + final float x1 = left - mGapBetweenPaths; final float y1 = bottom; - final float x2 = mInnerBottomLeftCorner.x; + final float x2 = mInnerBottomLeftCorner.x - mGapBetweenPaths; final float y2 = mInnerBottomLeftCorner.y; - final float x3 = mInnerBottomRightCorner.x; + final float x3 = mInnerBottomRightCorner.x + mGapBetweenPaths; final float y3 = mInnerBottomRightCorner.y; - final float x4 = right; + final float x4 = right + mGapBetweenPaths; final float y4 = bottom; drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); @@ -484,6 +498,10 @@ public class ReactViewBackgroundDrawable extends Drawable { mInnerClipPathForBorderRadius = new Path(); } + if (mBackgroundColorRenderPath == null) { + mBackgroundColorRenderPath = new Path(); + } + if (mOuterClipPathForBorderRadius == null) { mOuterClipPathForBorderRadius = new Path(); } @@ -513,6 +531,7 @@ public class ReactViewBackgroundDrawable extends Drawable { } mInnerClipPathForBorderRadius.reset(); + mBackgroundColorRenderPath.reset(); mOuterClipPathForBorderRadius.reset(); mPathForBorderRadiusOutline.reset(); mCenterDrawPath.reset(); @@ -634,6 +653,27 @@ public class ReactViewBackgroundDrawable extends Drawable { }, Path.Direction.CW); + // There is a small gap between mBackgroundColorRenderPath and its + // border. mGapBetweenPaths is used to slightly enlarge the rectangle + // (mInnerClipTempRectForBorderRadius), ensuring the border can be + // drawn on top without the gap. + mBackgroundColorRenderPath.addRoundRect( + mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths, + new float[] { + innerTopLeftRadiusX, + innerTopLeftRadiusY, + innerTopRightRadiusX, + innerTopRightRadiusY, + innerBottomRightRadiusX, + innerBottomRightRadiusY, + innerBottomLeftRadiusX, + innerBottomLeftRadiusY, + }, + Path.Direction.CW); + mOuterClipPathForBorderRadius.addRoundRect( mOuterClipTempRectForBorderRadius, new float[] {