|
7 | 7 |
|
8 | 8 | package com.facebook.react.views.text.internal.span |
9 | 9 |
|
10 | | -import android.text.style.StrikethroughSpan |
| 10 | +import android.graphics.Canvas |
| 11 | +import android.graphics.Color |
| 12 | +import android.os.Build |
| 13 | +import android.text.Layout |
| 14 | +import kotlin.math.max |
11 | 15 |
|
12 | | -/** Wraps [StrikethroughSpan] as a [ReactSpan]. */ |
13 | | -internal class ReactStrikethroughSpan : StrikethroughSpan(), ReactSpan |
| 16 | +/** |
| 17 | + * Draws a strikethrough whose color may differ from the text color. Subclasses |
| 18 | + * [DrawCommandSpan] so [PreparedLayoutTextView] and [ReactTextView] invoke |
| 19 | + * [onDraw] after the layout renders its text. We do NOT extend |
| 20 | + * [android.text.style.StrikethroughSpan] here: the framework's `Layout.draw` |
| 21 | + * paints the strikethrough using `paint.color` with no field to override, |
| 22 | + * so the only way to get a distinct color is to draw it ourselves. |
| 23 | + * |
| 24 | + * When [color] is [Color.TRANSPARENT] (the default when no |
| 25 | + * `textDecorationColor` prop was passed), the strikethrough is drawn in the |
| 26 | + * text's foreground color, matching the platform's prior behavior. |
| 27 | + */ |
| 28 | +internal class ReactStrikethroughSpan(private val color: Int = Color.TRANSPARENT) : |
| 29 | + DrawCommandSpan() { |
| 30 | + |
| 31 | + override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { |
| 32 | + val paint = layout.paint |
| 33 | + val savedColor = paint.color |
| 34 | + val savedStrokeWidth = paint.strokeWidth |
| 35 | + val savedStyle = paint.style |
| 36 | + val savedAntiAlias = paint.isAntiAlias |
| 37 | + val effectiveColor = if (color != Color.TRANSPARENT) color else savedColor |
| 38 | + val thickness = |
| 39 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| 40 | + max(paint.underlineThickness, 1.5f) |
| 41 | + } else { |
| 42 | + max(paint.fontMetrics.descent * 0.1f, 1.5f) |
| 43 | + } |
| 44 | + |
| 45 | + paint.color = effectiveColor |
| 46 | + paint.strokeWidth = thickness |
| 47 | + paint.style = android.graphics.Paint.Style.STROKE |
| 48 | + paint.isAntiAlias = true |
| 49 | + |
| 50 | + // Position the strikethrough at the midpoint between the line's top |
| 51 | + // and baseline so it sits near the x-height midline like the platform |
| 52 | + // default. `fontMetrics.ascent` is negative and `descent` is positive, |
| 53 | + // so the sum / 2 gives a small negative offset from the baseline. |
| 54 | + val fm = paint.fontMetrics |
| 55 | + val offset = (fm.ascent + fm.descent) / 2f |
| 56 | + |
| 57 | + val startLine = layout.getLineForOffset(start) |
| 58 | + val endLine = layout.getLineForOffset(end) |
| 59 | + for (line in startLine..endLine) { |
| 60 | + val baseline = layout.getLineBaseline(line).toFloat() |
| 61 | + val x1 = |
| 62 | + if (line == startLine) layout.getPrimaryHorizontal(start) else layout.getLineLeft(line) |
| 63 | + val x2 = |
| 64 | + if (line == endLine) layout.getPrimaryHorizontal(end) else layout.getLineRight(line) |
| 65 | + val y = baseline + offset |
| 66 | + canvas.drawLine(x1, y, x2, y, paint) |
| 67 | + } |
| 68 | + |
| 69 | + paint.color = savedColor |
| 70 | + paint.strokeWidth = savedStrokeWidth |
| 71 | + paint.style = savedStyle |
| 72 | + paint.isAntiAlias = savedAntiAlias |
| 73 | + } |
| 74 | +} |
0 commit comments