Jetpack compose - 在文本后面绘制背景

Jetpack compose - draw background behind text

我想在 Jetpack Compose 中的一些 AnnotatedString 后面绘制背景(Example here). Using the view system, we could do this by writing a custom text view - https://medium.com/androiddevelopers/drawing-a-rounded-corner-background-on-text-5a610a95af5。有没有办法使用 Jetpack Compose 做到这一点?

我正在查看 Textdraw 修饰符,但我似乎无法弄清楚如何获取行号或我需要的文本的 start/end绘制背景。

我应该使用 Canvas 而不是 Text 吗?

关于Text字符布局的主要信息来源是TextLayoutResult,可以通过onTextLayout参数接收

它 API 远非完美。 getPathForRange returns Path 这正是您所需要的,但与任何其他 Path 一样,它不能被修改,例如你将无法绕过这条路径的拐角,与普通 SpanStyle 背景

没有太大区别

getBoundingBox只returns帧一个字符。我玩了一下,得到了选定范围的 Rects 列表:

fun TextLayoutResult.getBoundingBoxesForRange(start: Int, end: Int): List<Rect> {
    var prevRect: Rect? = null
    var firstLineCharRect: Rect? = null
    val boundingBoxes = mutableListOf<Rect>()
    for (i in start..end) {
        val rect = getBoundingBox(i)
        val isLastRect = i == end
        
        // single char case
        if (isLastRect && firstLineCharRect == null) {
            firstLineCharRect = rect
            prevRect = rect
        }

        // `rect.right` is zero for the last space in each line
        // looks like an issue to me, reported: https://issuetracker.google.com/issues/197146630
        if (!isLastRect && rect.right == 0f) continue

        if (firstLineCharRect == null) {
            firstLineCharRect = rect
        } else if (prevRect != null) {
            if (prevRect.bottom != rect.bottom || isLastRect) {
                boundingBoxes.add(
                    firstLineCharRect.copy(right = prevRect.right)
                )
                firstLineCharRect = rect
            }
        }
        prevRect = rect
    }
    return boundingBoxes
}

现在您可以在 Canvas 上绘制这些矩形:

Box(Modifier.padding(10.dp)) {
    val text =
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    val selectedParts = listOf(
        "consectetur adipiscing",
        "officia deserunt",
        "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud",
        "consequat.",
    )
    var selectedPartPaths by remember { mutableStateOf(listOf<Path>()) }
    Text(
        text,
        style = MaterialTheme.typography.h6,
        onTextLayout = { layoutResult ->
            selectedPartPaths = selectedParts.map { part ->
                val cornerRadius = CornerRadius(x = 20f, y = 20f)
                Path().apply {
                    val startIndex = text.indexOf(part)
                    val boundingBoxes = layoutResult
                        .getBoundingBoxesForRange(
                            start = startIndex,
                            end = startIndex + part.count()
                        )
                    for (i in boundingBoxes.indices) {
                        val boundingBox = boundingBoxes[i]
                        val leftCornerRoundRect =
                            if (i == 0) cornerRadius else CornerRadius.Zero
                        val rightCornerRoundRect =
                            if (i == boundingBoxes.indices.last) cornerRadius else CornerRadius.Zero
                        addRoundRect(
                            RoundRect(
                                boundingBox.inflate(verticalDelta = -2f, horizontalDelta = 7f),
                                topLeft = leftCornerRoundRect,
                                topRight = rightCornerRoundRect,
                                bottomRight = rightCornerRoundRect,
                                bottomLeft = leftCornerRoundRect,
                            )
                        )
                    }
                }
            }
        },
        modifier = Modifier.drawBehind {
            selectedPartPaths.forEach { path ->
                drawPath(path, style = Fill, color = Color.Blue.copy(alpha = 0.2f))
                drawPath(path, style = Stroke(width = 2f), color = Color.Blue)
            }
        }
    )
}

fun Rect.inflate(verticalDelta: Float, horizontalDelta: Float) =
    Rect(
        left = left - horizontalDelta,
        top = top - verticalDelta,
        right = right + horizontalDelta,
        bottom = bottom + verticalDelta,
    )

结果: