From ae1c348f6661676a055d10a5eb545ea4740b06f7 Mon Sep 17 00:00:00 2001 From: Rishad Date: Thu, 19 Mar 2026 20:32:42 +0530 Subject: [PATCH 1/3] fix(Android): fix translucent dashed/dotted borders and alpha scaling Currently, rectangular borders with translucent colors on Android fall back to a 'slow path' that draws quadrilaterals using FILL style. This is incompatible with DashPathEffect which requires STROKE, causing dashed and dotted borders to render as solid lines. Additionally, a bug in multiplyColorAlpha was causing translucent borders to be incorrectly scaled (using a bit-shifted value instead of the actual alpha), making them appear nearly invisible. This fix: 1. Corrects the alpha multiplication logic in BorderDrawable.kt. 2. Allows dashed/dotted translucent borders to use the fast STROKE-based path. 3. Optimizes the fast path for uniform borders to use a single closed rectangular Path, eliminating dark overlapping corners and ensuring continuous dash patterns. --- .../uimanager/drawable/BorderDrawable.kt | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt index d1293ee48d9c..386e31169e54 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt @@ -254,49 +254,66 @@ internal class BorderDrawable( computedBorderColors.right, computedBorderColors.bottom, ) - if (fastBorderColor != 0) { + if (fastBorderColor != 0 && + (borderStyle != BorderStyle.SOLID || Color.alpha(fastBorderColor) == 255)) { if (Color.alpha(fastBorderColor) != 0) { // Border color is not transparent. val right = bounds.right val bottom = bounds.bottom borderPaint.color = multiplyColorAlpha(fastBorderColor, borderAlpha) borderPaint.style = Paint.Style.STROKE - pathForSingleBorder = Path() - if (borderLeft > 0) { + pathForSingleBorder = pathForSingleBorder ?: Path() + if (borderLeft == borderTop && borderLeft == borderRight && borderLeft == borderBottom && + borderLeft > 0) { + // All borders have same width. Draw single rect path to avoid corner overlapping + // and have continuous dashes across corners. + val width = borderLeft.toFloat() + updatePathEffect(width.toInt()) + borderPaint.strokeWidth = width pathForSingleBorder?.reset() - val width = borderWidth.left.roundToInt() - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo((left + width / 2).toFloat(), top.toFloat()) - pathForSingleBorder?.lineTo((left + width / 2).toFloat(), bottom.toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderTop > 0) { - pathForSingleBorder?.reset() - val width = borderWidth.top.roundToInt() - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo(left.toFloat(), (top + width / 2).toFloat()) - pathForSingleBorder?.lineTo(right.toFloat(), (top + width / 2).toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderRight > 0) { - pathForSingleBorder?.reset() - val width = borderWidth.right.roundToInt() - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo((right - width / 2).toFloat(), top.toFloat()) - pathForSingleBorder?.lineTo((right - width / 2).toFloat(), bottom.toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderBottom > 0) { - pathForSingleBorder?.reset() - val width = borderWidth.bottom.roundToInt() - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo(left.toFloat(), (bottom - width / 2).toFloat()) - pathForSingleBorder?.lineTo(right.toFloat(), (bottom - width / 2).toFloat()) + pathForSingleBorder?.addRect( + left + width / 2f, top + width / 2f, + right - width / 2f, bottom - width / 2f, + Path.Direction.CW + ) pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } else { + if (borderLeft > 0) { + pathForSingleBorder?.reset() + val width = borderLeft + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo((left + width / 2).toFloat(), top.toFloat()) + pathForSingleBorder?.lineTo((left + width / 2).toFloat(), bottom.toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderTop > 0) { + pathForSingleBorder?.reset() + val width = borderTop + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo(left.toFloat(), (top + width / 2).toFloat()) + pathForSingleBorder?.lineTo(right.toFloat(), (top + width / 2).toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderRight > 0) { + pathForSingleBorder?.reset() + val width = borderRight + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo((right - width / 2).toFloat(), top.toFloat()) + pathForSingleBorder?.lineTo((right - width / 2).toFloat(), bottom.toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderBottom > 0) { + pathForSingleBorder?.reset() + val width = borderBottom + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo(left.toFloat(), (bottom - width / 2).toFloat()) + pathForSingleBorder?.lineTo(right.toFloat(), (bottom - width / 2).toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } } } } else { @@ -493,16 +510,6 @@ internal class BorderDrawable( colorRight: Int, colorBottom: Int, ): Int { - // If any of the border colors are translucent then we can't use the fast path. - if ( - Color.alpha(colorLeft) < 255 || - Color.alpha(colorTop) < 255 || - Color.alpha(colorRight) < 255 || - Color.alpha(colorBottom) < 255 - ) { - return 0 - } - val andSmear = ((if (borderLeft > 0) colorLeft else ALL_BITS_SET) and (if (borderTop > 0) colorTop else ALL_BITS_SET) and @@ -1094,7 +1101,7 @@ internal class BorderDrawable( } val alpha = rawAlpha + (rawAlpha shr 7) // make it 0..256 val colorAlpha = color ushr 24 - val multipliedAlpha = colorAlpha * (alpha shr 7) shr 8 + val multipliedAlpha = colorAlpha * alpha shr 8 return (multipliedAlpha shl 24) or (color and 0x00FFFFFF) } } From 5b42fed2c2a2666aaa1c99a5cbce13148fefe27e Mon Sep 17 00:00:00 2001 From: Rishad Date: Fri, 20 Mar 2026 10:00:23 +0530 Subject: [PATCH 2/3] fix(Android): fix dashed/dotted rounded border rendering Ensure dashed and dotted border styles render correctly for rounded borders by updating the rounded border drawing path on Android. This change also resolves dash pattern calculation issues by using resolved border widths in pixels, improving rendering consistency across densities. --- .../uimanager/drawable/BorderDrawable.kt | 506 ++++++++++++------ 1 file changed, 339 insertions(+), 167 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt index 386e31169e54..c9041bd8b91d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt @@ -242,7 +242,9 @@ internal class BorderDrawable( val left = bounds.left val top = bounds.top - // Check for fast path to border drawing. + val right = bounds.right + val bottom = bounds.bottom + val fastBorderColor = fastBorderCompatibleColorOrZero( borderLeft, @@ -254,66 +256,84 @@ internal class BorderDrawable( computedBorderColors.right, computedBorderColors.bottom, ) - if (fastBorderColor != 0 && - (borderStyle != BorderStyle.SOLID || Color.alpha(fastBorderColor) == 255)) { - if (Color.alpha(fastBorderColor) != 0) { - // Border color is not transparent. - val right = bounds.right - val bottom = bounds.bottom + + val widths = listOf(borderLeft, borderTop, borderRight, borderBottom).filter { it > 0 } + val isUniformWidth = widths.isNotEmpty() && widths.all { it == widths[0] } + + // We use STROKE if: + // - It's not a solid border (dashed/dotted MUST use stroke). + // - OR it's a solid uniform color that is either opaque OR has uniform width (no overlaps). + // In other cases (non-uniform translucent solid or multi-colored solid), + // we use quadrilaterals (FILL) to get perfect joints. + val useStrokeLogic = + borderStyle != BorderStyle.SOLID || + (fastBorderColor != 0 && (Color.alpha(fastBorderColor) == 255 || isUniformWidth)) + + if (useStrokeLogic) { + if (fastBorderColor != 0 && Color.alpha(fastBorderColor) == 0) { + return + } + + borderPaint.style = Paint.Style.STROKE + pathForSingleBorder = pathForSingleBorder ?: Path() + + if (fastBorderColor != 0 && isUniformWidth && borderLeft > 0) { + // Optimized path: Use a single rect for uniform border width/color. + // This avoids overlapping corners for translucent borders and makes + // dashes/dots continuous around corners. + val width = borderLeft.toFloat() borderPaint.color = multiplyColorAlpha(fastBorderColor, borderAlpha) - borderPaint.style = Paint.Style.STROKE - pathForSingleBorder = pathForSingleBorder ?: Path() - if (borderLeft == borderTop && borderLeft == borderRight && borderLeft == borderBottom && - borderLeft > 0) { - // All borders have same width. Draw single rect path to avoid corner overlapping - // and have continuous dashes across corners. - val width = borderLeft.toFloat() - updatePathEffect(width.toInt()) - borderPaint.strokeWidth = width + updatePathEffect(width.toInt()) + borderPaint.strokeWidth = width + pathForSingleBorder?.reset() + pathForSingleBorder?.addRect( + left + width / 2f, top + width / 2f, + right - width / 2f, bottom - width / 2f, + Path.Direction.CW, + ) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } else { + // Side-by-side lines. This is used for multi-color dashed borders + // or non-uniform widths. + if (borderLeft > 0) { pathForSingleBorder?.reset() - pathForSingleBorder?.addRect( - left + width / 2f, top + width / 2f, - right - width / 2f, bottom - width / 2f, - Path.Direction.CW - ) + val width = borderLeft + borderPaint.color = multiplyColorAlpha(computedBorderColors.left, borderAlpha) + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo((left + width / 2).toFloat(), top.toFloat()) + pathForSingleBorder?.lineTo((left + width / 2).toFloat(), bottom.toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderTop > 0) { + pathForSingleBorder?.reset() + val width = borderTop + borderPaint.color = multiplyColorAlpha(computedBorderColors.top, borderAlpha) + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo(left.toFloat(), (top + width / 2).toFloat()) + pathForSingleBorder?.lineTo(right.toFloat(), (top + width / 2).toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderRight > 0) { + pathForSingleBorder?.reset() + val width = borderRight + borderPaint.color = multiplyColorAlpha(computedBorderColors.right, borderAlpha) + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo((right - width / 2).toFloat(), top.toFloat()) + pathForSingleBorder?.lineTo((right - width / 2).toFloat(), bottom.toFloat()) + pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } + } + if (borderBottom > 0) { + pathForSingleBorder?.reset() + val width = borderBottom + borderPaint.color = multiplyColorAlpha(computedBorderColors.bottom, borderAlpha) + updatePathEffect(width) + borderPaint.strokeWidth = width.toFloat() + pathForSingleBorder?.moveTo(left.toFloat(), (bottom - width / 2).toFloat()) + pathForSingleBorder?.lineTo(right.toFloat(), (bottom - width / 2).toFloat()) pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } else { - if (borderLeft > 0) { - pathForSingleBorder?.reset() - val width = borderLeft - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo((left + width / 2).toFloat(), top.toFloat()) - pathForSingleBorder?.lineTo((left + width / 2).toFloat(), bottom.toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderTop > 0) { - pathForSingleBorder?.reset() - val width = borderTop - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo(left.toFloat(), (top + width / 2).toFloat()) - pathForSingleBorder?.lineTo(right.toFloat(), (top + width / 2).toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderRight > 0) { - pathForSingleBorder?.reset() - val width = borderRight - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo((right - width / 2).toFloat(), top.toFloat()) - pathForSingleBorder?.lineTo((right - width / 2).toFloat(), bottom.toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } - if (borderBottom > 0) { - pathForSingleBorder?.reset() - val width = borderBottom - updatePathEffect(width) - borderPaint.strokeWidth = width.toFloat() - pathForSingleBorder?.moveTo(left.toFloat(), (bottom - width / 2).toFloat()) - pathForSingleBorder?.lineTo(right.toFloat(), (bottom - width / 2).toFloat()) - pathForSingleBorder?.let { canvas.drawPath(it, borderPaint) } - } } } } else { @@ -323,6 +343,7 @@ internal class BorderDrawable( * after drawing is done, we will re-enable it. */ borderPaint.isAntiAlias = false + borderPaint.style = Paint.Style.FILL val width = bounds.width() val height = bounds.height() if (borderLeft > 0) { @@ -369,7 +390,6 @@ internal class BorderDrawable( val y4 = (top + height - borderBottom).toFloat() drawQuadrilateral(canvas, computedBorderColors.bottom, x1, y1, x2, y2, x3, y3, x4, y4) } - // re-enable anti alias borderPaint.isAntiAlias = true } @@ -383,119 +403,247 @@ internal class BorderDrawable( // Clip outer border canvas.clipPath(checkNotNull(outerClipPathForBorderRadius)) - val borderWidth = computeBorderInsets() - if ( - borderWidth.top > 0 || - borderWidth.bottom > 0 || - borderWidth.left > 0 || - borderWidth.right > 0 - ) { - - // If it's a full and even border draw inner rect path with stroke - val fullBorderWidth: Float = getFullBorderWidth() - val borderColor = getBorderColor(LogicalEdge.ALL) - - if ( - borderWidth.top == fullBorderWidth && - borderWidth.bottom == fullBorderWidth && - borderWidth.left == fullBorderWidth && - borderWidth.right == fullBorderWidth && - computedBorderColors.left == borderColor && - computedBorderColors.top == borderColor && - computedBorderColors.right == borderColor && - computedBorderColors.bottom == borderColor - ) { - if (fullBorderWidth > 0) { - borderPaint.color = multiplyColorAlpha(borderColor, borderAlpha) - borderPaint.style = Paint.Style.STROKE - borderPaint.strokeWidth = fullBorderWidth - if (computedBorderRadius?.isUniform() == true) { - tempRectForCenterDrawPath?.let { - canvas.drawRoundRect( - it, - ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.horizontal ?: 0f) - - borderWidth.left * 0.5f), - ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.vertical ?: 0f) - - borderWidth.top * 0.5f), - borderPaint, - ) - } - } else { - canvas.drawPath(checkNotNull(centerDrawPath), borderPaint) + val borderInsets = computeBorderInsets() + val fullBorderWidth = getFullBorderWidth() + val borderColor = getBorderColor(LogicalEdge.ALL) + + val isUniformColor = + computedBorderColors.left == borderColor && + computedBorderColors.top == borderColor && + computedBorderColors.right == borderColor && + computedBorderColors.bottom == borderColor + + val isUniformWidth = + borderInsets.top == fullBorderWidth && + borderInsets.bottom == fullBorderWidth && + borderInsets.left == fullBorderWidth && + borderInsets.right == fullBorderWidth + + val useStroke = + borderStyle != BorderStyle.SOLID || + (isUniformColor && isUniformWidth && Color.alpha(borderColor) == 255) + + if (useStroke && isUniformColor && isUniformWidth) { + if (fullBorderWidth > 0) { + borderPaint.color = multiplyColorAlpha(borderColor, borderAlpha) + borderPaint.style = Paint.Style.STROKE + borderPaint.strokeWidth = fullBorderWidth + if (computedBorderRadius?.isUniform() == true) { + tempRectForCenterDrawPath?.let { + canvas.drawRoundRect( + it, + ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.horizontal ?: 0f) - + borderInsets.left * 0.5f), + ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.vertical ?: 0f) - + borderInsets.top * 0.5f), + borderPaint, + ) } + } else { + canvas.drawPath(checkNotNull(centerDrawPath), borderPaint) } } + } else if (useStroke) { + // In the case of uneven border widths/colors but dashed style, + // we draw the whole path multiple times and clip it to each quadrilateral. + borderPaint.style = Paint.Style.STROKE + + val outerClipTempRect = checkNotNull(outerClipTempRectForBorderRadius) + val left = outerClipTempRect.left + val right = outerClipTempRect.right + val top = outerClipTempRect.top + val bottom = outerClipTempRect.bottom + + val innerTopLeftCorner = checkNotNull(this.innerTopLeftCorner) + val innerTopRightCorner = checkNotNull(this.innerTopRightCorner) + val innerBottomLeftCorner = checkNotNull(this.innerBottomLeftCorner) + val innerBottomRightCorner = checkNotNull(this.innerBottomRightCorner) + + if (borderInsets.left > 0) { + drawClippedSide( + canvas, + LogicalEdge.LEFT, + left, + top - gapBetweenPaths, + innerTopLeftCorner.x, + innerTopLeftCorner.y - gapBetweenPaths, + innerBottomLeftCorner.x, + innerBottomLeftCorner.y + gapBetweenPaths, + left, + bottom + gapBetweenPaths, + ) + } + if (borderInsets.top > 0) { + drawClippedSide( + canvas, + LogicalEdge.TOP, + left - gapBetweenPaths, + top, + innerTopLeftCorner.x - gapBetweenPaths, + innerTopLeftCorner.y, + innerTopRightCorner.x + gapBetweenPaths, + innerTopRightCorner.y, + right + gapBetweenPaths, + top, + ) + } + if (borderInsets.right > 0) { + drawClippedSide( + canvas, + LogicalEdge.RIGHT, + right, + top - gapBetweenPaths, + innerTopRightCorner.x, + innerTopRightCorner.y - gapBetweenPaths, + innerBottomRightCorner.x, + innerBottomRightCorner.y + gapBetweenPaths, + right, + bottom + gapBetweenPaths, + ) + } + if (borderInsets.bottom > 0) { + drawClippedSide( + canvas, + LogicalEdge.BOTTOM, + left - gapBetweenPaths, + bottom, + innerBottomLeftCorner.x - gapBetweenPaths, + innerBottomLeftCorner.y, + innerBottomRightCorner.x + gapBetweenPaths, + innerBottomRightCorner.y, + right + gapBetweenPaths, + bottom, + ) + } + } else { // In the case of uneven border widths/colors draw quadrilateral in each direction - else { - borderPaint.style = Paint.Style.FILL - - // Clip inner border - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipOutPath(checkNotNull(innerClipPathForBorderRadius)) - } else { - @Suppress("DEPRECATION") - canvas.clipPath(checkNotNull(innerClipPathForBorderRadius), Region.Op.DIFFERENCE) - } - val outerClipTempRect = checkNotNull(outerClipTempRectForBorderRadius) - val left = outerClipTempRect.left - val right = outerClipTempRect.right - val top = outerClipTempRect.top - val bottom = outerClipTempRect.bottom + borderPaint.style = Paint.Style.FILL - val innerTopLeftCorner = checkNotNull(this.innerTopLeftCorner) - val innerTopRightCorner = checkNotNull(this.innerTopRightCorner) - val innerBottomLeftCorner = checkNotNull(this.innerBottomLeftCorner) - val innerBottomRightCorner = checkNotNull(this.innerBottomRightCorner) + // Clip inner border + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(checkNotNull(innerClipPathForBorderRadius)) + } else { + @Suppress("DEPRECATION") + canvas.clipPath(checkNotNull(innerClipPathForBorderRadius), Region.Op.DIFFERENCE) + } + val outerClipTempRect = checkNotNull(outerClipTempRectForBorderRadius) + val left = outerClipTempRect.left + val right = outerClipTempRect.right + val top = outerClipTempRect.top + val bottom = outerClipTempRect.bottom + + val innerTopLeftCorner = checkNotNull(this.innerTopLeftCorner) + val innerTopRightCorner = checkNotNull(this.innerTopRightCorner) + val innerBottomLeftCorner = checkNotNull(this.innerBottomLeftCorner) + val innerBottomRightCorner = checkNotNull(this.innerBottomRightCorner) + + if (borderInsets.left > 0) { + val x1 = left + val y1: Float = top - gapBetweenPaths + val x2 = innerTopLeftCorner.x + val y2: Float = innerTopLeftCorner.y - gapBetweenPaths + val x3 = innerBottomLeftCorner.x + val y3: Float = innerBottomLeftCorner.y + gapBetweenPaths + val x4 = left + val y4: Float = bottom + gapBetweenPaths + drawQuadrilateral(canvas, computedBorderColors.left, x1, y1, x2, y2, x3, y3, x4, y4) + } + if (borderInsets.top > 0) { + val x1: Float = left - gapBetweenPaths + val y1 = top + val x2: Float = innerTopLeftCorner.x - gapBetweenPaths + val y2 = innerTopLeftCorner.y + val x3: Float = innerTopRightCorner.x + gapBetweenPaths + val y3 = innerTopRightCorner.y + val x4: Float = right + gapBetweenPaths + val y4 = top + drawQuadrilateral(canvas, computedBorderColors.top, x1, y1, x2, y2, x3, y3, x4, y4) + } + if (borderInsets.right > 0) { + val x1 = right + val y1: Float = top - gapBetweenPaths + val x2 = innerTopRightCorner.x + val y2: Float = innerTopRightCorner.y - gapBetweenPaths + val x3 = innerBottomRightCorner.x + val y3: Float = innerBottomRightCorner.y + gapBetweenPaths + val x4 = right + val y4: Float = bottom + gapBetweenPaths + drawQuadrilateral(canvas, computedBorderColors.right, x1, y1, x2, y2, x3, y3, x4, y4) + } + if (borderInsets.bottom > 0) { + val x1: Float = left - gapBetweenPaths + val y1 = bottom + val x2: Float = innerBottomLeftCorner.x - gapBetweenPaths + val y2 = innerBottomLeftCorner.y + val x3: Float = innerBottomRightCorner.x + gapBetweenPaths + val y3 = innerBottomRightCorner.y + val x4: Float = right + gapBetweenPaths + val y4 = bottom + drawQuadrilateral(canvas, computedBorderColors.bottom, x1, y1, x2, y2, x3, y3, x4, y4) + } + } + canvas.restore() + } - /** - * gapBetweenPaths is used to close the gap between the diagonal edges of the quadrilaterals - * on adjacent sides of the rectangle - */ - if (borderWidth.left > 0) { - val x1 = left - val y1: Float = top - gapBetweenPaths - val x2 = innerTopLeftCorner.x - val y2: Float = innerTopLeftCorner.y - gapBetweenPaths - val x3 = innerBottomLeftCorner.x - val y3: Float = innerBottomLeftCorner.y + gapBetweenPaths - val x4 = left - val y4: Float = bottom + gapBetweenPaths - drawQuadrilateral(canvas, computedBorderColors.left, x1, y1, x2, y2, x3, y3, x4, y4) - } - if (borderWidth.top > 0) { - val x1: Float = left - gapBetweenPaths - val y1 = top - val x2: Float = innerTopLeftCorner.x - gapBetweenPaths - val y2 = innerTopLeftCorner.y - val x3: Float = innerTopRightCorner.x + gapBetweenPaths - val y3 = innerTopRightCorner.y - val x4: Float = right + gapBetweenPaths - val y4 = top - drawQuadrilateral(canvas, computedBorderColors.top, x1, y1, x2, y2, x3, y3, x4, y4) - } - if (borderWidth.right > 0) { - val x1 = right - val y1: Float = top - gapBetweenPaths - val x2 = innerTopRightCorner.x - val y2: Float = innerTopRightCorner.y - gapBetweenPaths - val x3 = innerBottomRightCorner.x - val y3: Float = innerBottomRightCorner.y + gapBetweenPaths - val x4 = right - val y4: Float = bottom + gapBetweenPaths - drawQuadrilateral(canvas, computedBorderColors.right, x1, y1, x2, y2, x3, y3, x4, y4) + private fun drawClippedSide( + canvas: Canvas, + edge: LogicalEdge, + x1: Float, + y1: Float, + x2: Float, + y2: Float, + x3: Float, + y3: Float, + x4: Float, + y4: Float, + ) { + canvas.save() + if (this.pathForBorder == null) { + this.pathForBorder = Path() + } + pathForBorder?.reset() + pathForBorder?.moveTo(x1, y1) + pathForBorder?.lineTo(x2, y2) + pathForBorder?.lineTo(x3, y3) + pathForBorder?.lineTo(x4, y4) + pathForBorder?.close() + canvas.clipPath(pathForBorder!!) + + val sideColor = + when (edge) { + LogicalEdge.LEFT -> computedBorderColors.left + LogicalEdge.TOP -> computedBorderColors.top + LogicalEdge.RIGHT -> computedBorderColors.right + LogicalEdge.BOTTOM -> computedBorderColors.bottom + else -> computedBorderColors.left } - if (borderWidth.bottom > 0) { - val x1: Float = left - gapBetweenPaths - val y1 = bottom - val x2: Float = innerBottomLeftCorner.x - gapBetweenPaths - val y2 = innerBottomLeftCorner.y - val x3: Float = innerBottomRightCorner.x + gapBetweenPaths - val y3 = innerBottomRightCorner.y - val x4: Float = right + gapBetweenPaths - val y4 = bottom - drawQuadrilateral(canvas, computedBorderColors.bottom, x1, y1, x2, y2, x3, y3, x4, y4) + borderPaint.color = multiplyColorAlpha(sideColor, borderAlpha) + + val insets = computeBorderInsets() + val width = + when (edge) { + LogicalEdge.LEFT -> insets.left + LogicalEdge.TOP -> insets.top + LogicalEdge.RIGHT -> insets.right + LogicalEdge.BOTTOM -> insets.bottom + else -> 0f } + + updatePathEffect(width.toInt()) + borderPaint.strokeWidth = width + + if (computedBorderRadius?.isUniform() == true) { + tempRectForCenterDrawPath?.let { + canvas.drawRoundRect( + it, + ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.horizontal ?: 0f) - + insets.left * 0.5f), + ((computedBorderRadius?.topLeft?.toPixelFromDIP()?.vertical ?: 0f) - insets.top * 0.5f), + borderPaint, + ) } + } else { + canvas.drawPath(checkNotNull(centerDrawPath), borderPaint) } canvas.restore() } @@ -565,17 +713,41 @@ internal class BorderDrawable( return RectF(0f, 0f, 0f, 0f) } - /** For rounded borders we use default "borderWidth" property. */ + /** For rounded borders we use the maximum of the resolved border widths as a representative width. */ + private fun getDashWidth(): Float { + if (borderWidth == null) return 0f + + // Check Spacing.ALL first + val allWidth = borderWidth.getRaw(Spacing.ALL) + if (!allWidth.isNaN()) return allWidth.dpToPx() + + // Use the max of all individual sides as a representative width + val top = borderWidth.getRaw(Spacing.TOP) + val left = borderWidth.getRaw(Spacing.LEFT) + val right = borderWidth.getRaw(Spacing.RIGHT) + val bottom = borderWidth.getRaw(Spacing.BOTTOM) + + val maxSide = + maxOf( + if (top.isNaN()) 0f else top, + maxOf( + if (left.isNaN()) 0f else left, + maxOf(if (right.isNaN()) 0f else right, if (bottom.isNaN()) 0f else bottom), + ), + ) + return maxSide.dpToPx() + } + private fun getFullBorderWidth(): Float { - val borderWidth = this.borderWidth?.getRaw(Spacing.ALL) ?: Float.NaN - return if (!borderWidth.isNaN()) borderWidth else 0f + val insets = computeBorderInsets() + return maxOf(maxOf(insets.left, insets.top), maxOf(insets.right, insets.bottom)) } private fun updatePathEffect() { /** Used for rounded border and rounded background. */ this.borderStyle?.let { style -> val pathEffectForBorderStyle = - if (this.borderStyle != null) getPathEffect(style, getFullBorderWidth()) else null + if (this.borderStyle != null) getPathEffect(style, getDashWidth()) else null borderPaint.setPathEffect(pathEffectForBorderStyle) } } From 25cc41f5ec37085cbed44a2f2714deab47bc9d23 Mon Sep 17 00:00:00 2001 From: Rishad Date: Fri, 20 Mar 2026 11:00:06 +0530 Subject: [PATCH 3/3] fix(Android): Adjust dash pattern scaling for consistent "tighter" look - Resolve dash patterns using raw DP values instead of density-scaled pixels to match the traditional React Native appearance on Android. - Consolidate dash sizing across rectangular and rounded borders by converting pixel widths back to DP in updatePathEffect. - Restore the original scale of dashes and dots for all high-DPI displays. --- .../com/facebook/react/uimanager/drawable/BorderDrawable.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt index c9041bd8b91d..2ee406e2ad16 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt @@ -719,7 +719,7 @@ internal class BorderDrawable( // Check Spacing.ALL first val allWidth = borderWidth.getRaw(Spacing.ALL) - if (!allWidth.isNaN()) return allWidth.dpToPx() + if (!allWidth.isNaN()) return allWidth // Use the max of all individual sides as a representative width val top = borderWidth.getRaw(Spacing.TOP) @@ -735,7 +735,7 @@ internal class BorderDrawable( maxOf(if (right.isNaN()) 0f else right, if (bottom.isNaN()) 0f else bottom), ), ) - return maxSide.dpToPx() + return maxSide } private fun getFullBorderWidth(): Float { @@ -755,7 +755,7 @@ internal class BorderDrawable( private fun updatePathEffect(borderWidth: Int) { this.borderStyle?.let { style -> val pathEffectForBorderStyle = - if (this.borderStyle != null) getPathEffect(style, borderWidth.toFloat()) else null + if (this.borderStyle != null) getPathEffect(style, borderWidth.pxToDp()) else null borderPaint.setPathEffect(pathEffectForBorderStyle) } }