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
This commit is contained in:
Sharon Zheng
2022-11-10 19:09:51 -08:00
committed by Facebook GitHub Bot
parent 6743d15329
commit 29caed22cc
2 changed files with 65 additions and 19 deletions
@@ -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;
}
@@ -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[] {