From 73efe7d6c3c02f34e76d93cb781943951c002bab Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 8 Jun 2026 08:24:07 -0700 Subject: [PATCH] Fix text decoration line thickness regression on Android Summary: D104680895 replaced the native Android `UnderlineSpan`/`StrikethroughSpan` with custom `CanvasEffectSpan` canvas drawing to support `textDecorationStyle`. The custom drawing enforces a minimum stroke thickness of `1.5f * density` (1.5dp), which is noticeably thicker than what the native framework spans produce. This diff removes the 1.5dp minimum for SOLID style so the decoration thickness matches the native `Paint.getUnderlineThickness()` value, restoring the pre-D104680895 visual behavior. The minimum is kept for all other styles (DOUBLE, DOTTED, DASHED, WAVY) since they need the thickness for their visual patterns to render correctly (dash intervals, dot sizes, bezier wavelength). **Remaining work:** the underline position (`baseline + thickness + 1f` in `ReactUnderlineSpan.kt`) also depends on thickness, so with a thinner SOLID stroke the underline sits closer to the text than before D104680895. The pre-D104680895 behavior used the native framework `UnderlineSpan` which positions the underline via `Paint.getUnderlinePosition()`. Attempted using `baseline + paint.underlinePosition + thickness / 2f` and `+ thickness` on API 29+ but neither matched the native positioning exactly. This needs further investigation to match the original vertical gap between text and underline. Differential Revision: D107866867 --- .../react/views/text/TextDecorationStyle.kt | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt index acd4200eb55..65294940ee5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt @@ -65,8 +65,8 @@ private val dashedEffects = * allocations. * * WAVY wavelength and control-point distance follow Chromium/Blink's `decoration_line_painter.cc`. - * DOUBLE uses a wider gap than Blink (`thickness + 2` vs `thickness + 1`) to keep both strokes - * visually distinct with antialiasing on mobile displays. + * DOUBLE uses a density-scaled gap (`thickness + 2dp`) to keep both strokes visually distinct with + * antialiasing on mobile displays. */ private fun drawDecorationLine( canvas: Canvas, @@ -75,13 +75,14 @@ private fun drawDecorationLine( x2: Float, y: Float, thickness: Float, + density: Float, style: TextDecorationStyle, reusablePath: Path, ) { when (style) { TextDecorationStyle.SOLID -> canvas.drawLine(x1, y, x2, y, paint) TextDecorationStyle.DOUBLE -> { - val gap = thickness + 2f + val gap = thickness + 2f * density canvas.drawLine(x1, y, x2, y, paint) canvas.drawLine(x1, y + gap, x2, y + gap, paint) } @@ -140,12 +141,20 @@ internal fun drawSpannedDecoration( if (fgSpans != null && fgSpans.isNotEmpty()) fgSpans.last().foregroundColor else textPaint.color } - val minThickness = 1.5f * textPaint.density val thickness = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - max(textPaint.underlineThickness, minThickness) + if (style == TextDecorationStyle.SOLID || style == TextDecorationStyle.DOUBLE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + textPaint.underlineThickness + } else { + textPaint.fontMetrics.descent * 0.1f + } } else { - max(textPaint.fontMetrics.descent * 0.1f, minThickness) + val minThickness = 1.5f * textPaint.density + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + max(textPaint.underlineThickness, minThickness) + } else { + max(textPaint.fontMetrics.descent * 0.1f, minThickness) + } } decorationPaint.set(textPaint) @@ -176,6 +185,16 @@ internal fun drawSpannedDecoration( val x1 = min(rawX1, rawX2) val x2 = max(rawX1, rawX2) val y = yOffsetForLine(decorationPaint, baseline, thickness) - drawDecorationLine(canvas, decorationPaint, x1, x2, y, thickness, style, scratchPath) + drawDecorationLine( + canvas, + decorationPaint, + x1, + x2, + y, + thickness, + textPaint.density, + style, + scratchPath, + ) } }