如何在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), ) }
实现思路
- 预计算每个时间线项的总高度(包含所有间距和文本高度),以及副标题的文本布局
- 累加所有项的高度得到整个Canvas的总高度
- 在单个Canvas中按顺序绘制每个项:从
currentY开始绘制标题、副标题,然后绘制对应的连线和圆点,绘制完成后更新currentY到下一个项的起始位置
内容的提问来源于stack exchange,提问作者Compose Learner
相关产品推荐
相关产品推荐

