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

Jetpack Compose Canvas绘制线条增多的UI性能问题求助

Jetpack Compose绘画应用性能优化方案

针对线条增多导致Canvas重绘卡顿的问题,以下是几个不修改核心lines历史结构的优化方案:

1. 离线Bitmap缓存历史绘制内容

将已完成的线条预先绘制到Bitmap中,Compose Canvas仅需绘制这个Bitmap,避免每次重组都遍历所有线条调用drawLine。

实现代码

data class Line(val start: Offset, val end: Offset, val color: Color = Color.Black, val strokeWidth: Dp = 5.dp)

@Composable
fun App() {
    var lines = remember { mutableStateListOf<Line>() }
    // 缓存历史绘制内容的Bitmap及对应Canvas
    val bitmapState = remember { mutableStateOf<Bitmap?>(null) }
    val density = LocalDensity.current
    val configuration = LocalConfiguration.current

    // 当lines列表变化时,更新Bitmap缓存
    LaunchedEffect(lines, configuration.screenWidthDp, configuration.screenHeightDp) {
        val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() }.toInt()
        val screenHeight = with(density) { configuration.screenHeightDp.dp.toPx() }.toInt()
        
        val bitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
        val canvas = android.graphics.Canvas(bitmap)

        lines.forEach { line ->
            val paint = Paint().apply {
                color = line.color.toArgb()
                strokeWidth = with(density) { line.strokeWidth.toPx() }
                strokeCap = Paint.Cap.ROUND
                style = Paint.Style.STROKE
            }
            canvas.drawLine(
                line.start.x,
                line.start.y,
                line.end.x,
                line.end.y,
                paint
            )
        }
        bitmapState.value = bitmap
    }

    Canvas(
        modifier = Modifier.fillMaxSize().pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                change.consume()
                val line = Line(
                    start = change.position - dragAmount,
                    end = change.position,
                    color = Color.Black, // 替换为你的strokeColor
                    strokeWidth = 5.dp // 替换为你的strokeWidth
                )
                lines.add(line)
            }
        }
    ) {
        bitmapState.value?.let { bitmap ->
            drawImage(ImageBitmap.bitmapAsImageBitmap(bitmap))
        }
    }
}

补充说明:代码中加入了屏幕尺寸监听,可自动适配屏幕旋转等配置变化;如果需要实时显示正在绘制的线条,可以在Canvas中额外绘制最后一条线条,避免等待LaunchedEffect执行的延迟。

2. 合并同属性连续线条为Path

保留lines历史结构的同时,在绘制时将相同颜色、相同宽度的连续线条合并成单个Path,减少drawLine的调用次数,降低绘制开销。

实现代码

@Composable
fun App() {
    var lines = remember { mutableStateListOf<Line>() }
    val density = LocalDensity.current

    Canvas(
        modifier = Modifier.fillMaxSize().pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                change.consume()
                val line = Line(
                    start = change.position - dragAmount,
                    end = change.position,
                    color = Color.Black,
                    strokeWidth = 5.dp
                )
                lines.add(line)
            }
        }
    ) {
        if (lines.isEmpty()) return@Canvas
        
        var currentPath = Path().apply { moveTo(lines[0].start.x, lines[0].start.y) }
        var currentColor = lines[0].color
        var currentStrokeWidth = lines[0].strokeWidth

        lines.forEachIndexed { index, line ->
            // 当前线条属性与上一个不同时,先绘制之前的Path
            if (line.color != currentColor || line.strokeWidth != currentStrokeWidth) {
                drawPath(
                    path = currentPath,
                    color = currentColor,
                    style = Stroke(
                        width = with(density) { currentStrokeWidth.toPx() },
                        cap = StrokeCap.Round
                    )
                )
                currentPath = Path().apply { moveTo(line.start.x, line.start.y) }
                currentColor = line.color
                currentStrokeWidth = line.strokeWidth
            }
            currentPath.lineTo(line.end.x, line.end.y)
            
            // 绘制最后一组Path
            if (index == lines.lastIndex) {
                drawPath(
                    path = currentPath,
                    color = currentColor,
                    style = Stroke(
                        width = with(density) { currentStrokeWidth.toPx() },
                        cap = StrokeCap.Round
                    )
                )
            }
        }
    }
}

这个方案在大量连续线条属性相同时性能提升明显,同时完全保留了每条Line的历史数据,不影响回退功能。

3. 局部重绘优化

如果仅在拖拽区域有新线条生成,可通过Modifier.graphicsLayer指定重绘区域,避免整个Canvas重绘。可结合前两个方案使用,进一步降低性能消耗:

Canvas(
    modifier = Modifier.fillMaxSize()
        .graphicsLayer {
            // 仅重绘最新线条所在的局部区域,需根据实际拖拽位置计算
            val latestLine = lines.lastOrNull()
            latestLine?.let {
                val padding = with(density) { it.strokeWidth.toPx() }
                clipBounds = Rect(
                    left = min(it.start.x, it.end.x) - padding,
                    top = min(it.start.y, it.end.y) - padding,
                    right = max(it.start.x, it.end.x) + padding,
                    bottom = max(it.start.y, it.end.y) + padding
                )
            }
        }
        .pointerInput(Unit) { /* 拖拽逻辑不变 */ }
) {
    // 绘制逻辑(配合Bitmap或Path合并方案)
}

内容的提问来源于stack exchange,提问作者Lakshya Sukhralia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 02:05:36