Skip to content

Commit

Permalink
rounded Views with borders shows small gap
Browse files Browse the repository at this point in the history
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
  • Loading branch information
skinsshark authored and facebook-github-bot committed Nov 11, 2022
1 parent 6743d15 commit 29caed2
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -86,6 +87,7 @@ private enum BorderStyle {
private @Nullable BorderStyle mBorderStyle;

private @Nullable Path mInnerClipPathForBorderRadius;
private @Nullable Path mBackgroundColorRenderPath;
private @Nullable Path mOuterClipPathForBorderRadius;
private @Nullable Path mPathForBorderRadiusOutline;
private @Nullable Path mPathForBorder;
Expand All @@ -107,6 +109,13 @@ private enum BorderStyle {
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;
Expand Down Expand Up @@ -329,11 +338,15 @@ private void drawRoundedBackgroundWithBorders(Canvas canvas) {
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();
Expand Down Expand Up @@ -369,8 +382,7 @@ private void drawRoundedBackgroundWithBorders(Canvas canvas) {
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;
Expand Down Expand Up @@ -416,53 +428,55 @@ private void drawRoundedBackgroundWithBorders(Canvas canvas) {
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);
}

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);
Expand All @@ -484,6 +498,10 @@ private void updatePath() {
mInnerClipPathForBorderRadius = new Path();
}

if (mBackgroundColorRenderPath == null) {
mBackgroundColorRenderPath = new Path();
}

if (mOuterClipPathForBorderRadius == null) {
mOuterClipPathForBorderRadius = new Path();
}
Expand Down Expand Up @@ -513,6 +531,7 @@ private void updatePath() {
}

mInnerClipPathForBorderRadius.reset();
mBackgroundColorRenderPath.reset();
mOuterClipPathForBorderRadius.reset();
mPathForBorderRadiusOutline.reset();
mCenterDrawPath.reset();
Expand Down Expand Up @@ -634,6 +653,27 @@ private void updatePath() {
},
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[] {
Expand Down

0 comments on commit 29caed2

Please sign in to comment.