You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中避免多个Canvas视图互相重叠?

时间线视图重叠问题解决及单Canvas实现方案

问题现状

用多个Canvas组件实现时间线视图时,出现内容互相重叠的情况:添加背景色后,每个项的文本会和其他视图重叠,同时不清楚如何用单个Canvas实现完整时间线。

问题原因

计算canvasHeightInDp时只累加了标题和副标题的文本高度,但没有包含viewMarginTop、subTitleTopMargin这些垂直方向的间距,导致Canvas的实际高度不足以容纳所有绘制内容,内容溢出到下一个Canvas区域。


方案一:修复多Canvas重叠问题

调整canvasHeightInDp的计算逻辑,把所有垂直方向的间距都加入高度计算:

修改后的CanvasView关键代码

val canvasHeightInDp = LocalDensity.current.run {
    // 加入viewMarginTop和subTitleTopMargin的高度,再加上文本高度
    viewMarginTop.toDp() + 
    titleTextLayoutResult.size.height.toDp() +
    subTitleTopMargin.toDp() +
    multipleTextLayoutResult.size.height.toDp()
}

同时,在CanvasContent中调整副标题的绘制Y坐标,避免和标题重叠:

drawText(
    textLayoutResult = multipleTextLayoutResult,
    topLeft = Offset(
        x = center.x - titleTextLayoutResult.size.width / 2,
        // 直接用viewMarginTop + 标题完整高度 + subTitleTopMargin,不用除以2
        y = viewMarginTop + titleTextLayoutResult.size.height + subTitleTopMargin,
    )
)

这样每个Canvas的高度就能完全容纳自身内容,不会出现重叠。


方案二:改用单个Canvas实现完整时间线

如果想用单个Canvas绘制所有时间线项,需要先计算每个项的总高度,然后按顺序绘制每个项的内容、连线和圆点:

完整单Canvas实现代码

@OptIn(ExperimentalTextApi::class)
@Composable
fun SingleCanvasTimeLine() {
    val titleTextMeasurer = rememberTextMeasurer()
    val timeLineList = getTimeLineDataList()
    val typeToColorMap = mapOf(
        "app" to Color.Black,
        "link" to Color.Blue
    )
    val titleText = AnnotatedString("Hello world!!")
    val titleTextLayoutResult = remember {
        titleTextMeasurer.measure(titleText, TextStyle(fontSize = 16.sp))
    }
    val endPadding = with(LocalDensity.current) { 10.dp.toPx() }
    val circleSize = with(LocalDensity.current) { 6.dp.toPx() }
    val viewMarginTop = with(LocalDensity.current) { 24.dp.toPx() }
    val subTitleTopMargin = with(LocalDensity.current) { 12.dp.toPx() }
    val lineStrokeWidth = with(LocalDensity.current) { 2.dp.toPx() }

    // 预计算每个时间线项的总高度和文本布局
    val itemLayouts = remember(timeLineList) {
        timeLineList.map { item ->
            val multipleString = buildAnnotatedString {
                item.items.forEach { (text, type) ->
                    withStyle(SpanStyle(color = typeToColorMap[type] ?: Color.Red)) {
                        append("$text\n")
                    }
                }
            }
            val subTextLayout = titleTextMeasurer.measure(
                multipleString,
                TextStyle(fontSize = 14.sp, lineHeight = 1.3.em)
            )
            val itemTotalHeight = viewMarginTop + titleTextLayoutResult.size.height + subTitleTopMargin + subTextLayout.size.height
            ItemLayout(subTextLayout, itemTotalHeight)
        }
    }

    // 计算整个Canvas的总高度
    val totalCanvasHeight = with(LocalDensity.current) {
        itemLayouts.sumOf { it.totalHeight }.toDp()
    }

    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(totalCanvasHeight)
            .background(Color.LightGray)
    ) {
        val circleX = (center.x / 2) + (titleTextLayoutResult.size.width / 2) - endPadding
        var currentY = 0f

        itemLayouts.forEachIndexed { index, layout ->
            // 绘制标题
            drawText(
                textLayoutResult = titleTextLayoutResult,
                topLeft = Offset(
                    x = center.x - titleTextLayoutResult.size.width / 2,
                    y = currentY + viewMarginTop
                )
            )
            // 绘制副标题
            drawText(
                textLayoutResult = layout.subTextLayout,
                topLeft = Offset(
                    x = center.x - titleTextLayoutResult.size.width / 2,
                    y = currentY + viewMarginTop + titleTextLayoutResult.size.height + subTitleTopMargin
                )
            )

            val circleY = currentY + viewMarginTop + titleTextLayoutResult.size.height / 2
            // 绘制上方连线(非第一个项)
            if (index != 0) {
                drawLine(
                    color = Color.Black,
                    start = Offset(circleX, currentY),
                    end = Offset(circleX, circleY - circleSize),
                    strokeWidth = lineStrokeWidth
                )
            }
            // 绘制圆点
            drawCircle(
                color = Color.Black,
                radius = circleSize,
                center = Offset(circleX, circleY)
            )
            // 绘制下方连线(非最后一个项)
            if (index != itemLayouts.lastIndex) {
                drawLine(
                    color = Color.Black,
                    start = Offset(circleX, circleY + circleSize),
                    end = Offset(circleX, currentY + layout.totalHeight),
                    strokeWidth = lineStrokeWidth
                )
            }

            // 移动到下一个项的起始Y坐标
            currentY += layout.totalHeight
        }
    }
}

// 用于存储每个项的布局信息
private data class ItemLayout(
    val subTextLayout: TextLayoutResult,
    val totalHeight: Float
)

// 模拟数据
data class TimeLineData(val items: List<Pair<String, String>>)

private fun getTimeLineDataList(): List<TimeLineData> {
    val firstList = listOf(
        Pair("Text 1", "app"),
        Pair("Text 2", "link"),
    )
    val secondList = listOf(
        Pair("Text 1", "app"),
        Pair("Text 2", "link"),
        Pair("Text 3", "app"),
        Pair("Text 4", "link"),
        Pair("Text 5", ""),
    )
    val thirdList = listOf(
        Pair("Text 1", "app"),
    )
    val fourList = listOf(
        Pair("Text 1", "app"),
        Pair("Text 2", "link"),
        Pair("Text 3", "app"),
    )
    return listOf(
        TimeLineData(firstList),
        TimeLineData(secondList),
        TimeLineData(emptyList()),
        TimeLineData(thirdList),
        TimeLineData(fourList),
    )
}

实现思路

  1. 预计算每个时间线项的总高度(包含所有间距和文本高度),以及副标题的文本布局
  2. 累加所有项的高度得到整个Canvas的总高度
  3. 在单个Canvas中按顺序绘制每个项:从currentY开始绘制标题、副标题,然后绘制对应的连线和圆点,绘制完成后更新currentY到下一个项的起始位置

内容的提问来源于stack exchange,提问作者Compose Learner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 07:25:01