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

如何在Android Jetpack Compose中基于触摸事件实现大规模绘画

解决方案

你遇到的是Compose手写/画板场景典型的全量重绘性能问题,这里有两种经过验证的可行方案:


方案1:离屏Bitmap缓存(推荐,性能最优,适配绝大多数场景)

这个方案完全对标传统View体系里用Bitmap做绘制缓存的思路,核心逻辑是所有历史绘制内容都直接写到离屏Bitmap里,每次Canvas重组只需要绘制缓存好的Bitmap加上当前正在输入的实时路径,不需要遍历任何历史触摸数据,哪怕累计几十万条触摸事件也不会出现性能衰减。

核心实现代码如下:

@Composable
fun HighPerformanceDrawingBoard() {
    // 存储所有历史绘制内容的离屏位图
    val cachedBitmap = remember { mutableStateOf<ImageBitmap?>(null) }
    // 绑定离屏位图的Canvas,所有历史绘制操作直接写入该Canvas
    val offscreenCanvas = remember { mutableStateOf<androidx.compose.ui.graphics.Canvas?>(null) }
    // 记录当前手指未抬起时的实时绘制路径
    val currentDrawingPath = remember { Path() }
    // 绘制所用的画笔配置,可根据需求自定义颜色、粗细等参数
    val drawPaint = remember {
        Paint().apply {
            color = Color.Black
            strokeWidth = 4.dp.toPx()
            style = PaintingStyle.Stroke
            strokeCap = StrokeCap.Round
            strokeJoin = StrokeJoin.Round
            isAntiAlias = true
        }
    }

    // 触摸事件处理
    val gestureModifier = Modifier.pointerInput(Unit) {
        detectDragGestures(
            onDragStart = { touchOffset ->
                currentDrawingPath.moveTo(touchOffset.x, touchOffset.y)
            },
            onDrag = { inputChange, _ ->
                inputChange.consume()
                val currentOffset = inputChange.position
                currentDrawingPath.lineTo(currentOffset.x, currentOffset.y)
                // 将新的路径段直接写入离屏缓存
                offscreenCanvas.value?.drawPath(currentDrawingPath, drawPaint)
                // 重置实时路径,避免重复绘制
                currentDrawingPath.reset()
                currentDrawingPath.moveTo(currentOffset.x, currentOffset.y)
            },
            onDragEnd = {
                currentDrawingPath.reset()
            },
            onDragCancel = {
                currentDrawingPath.reset()
            }
        )
    }

    Canvas(
        modifier = gestureModifier.fillMaxSize()
    ) {
        // 首次绘制时初始化缓存位图和离屏Canvas,仅执行一次
        if (cachedBitmap.value == null) {
            val bitmapWidth = size.width.toInt()
            val bitmapHeight = size.height.toInt()
            cachedBitmap.value = ImageBitmap(bitmapWidth, bitmapHeight)
            offscreenCanvas.value = Canvas(cachedBitmap.value!!)
            // 填充初始背景色
            offscreenCanvas.value?.drawRect(Color.White)
        }
        // 仅绘制缓存位图 + 当前实时路径,无历史遍历操作
        cachedBitmap.value?.let { drawImage(it) }
        drawPath(currentDrawingPath, drawPaint)
    }
}

方案2:自定义Painter封装缓存逻辑

如果你需要把绘制逻辑封装成可复用的组件,可以通过自定义Painter实现缓存能力,核心逻辑和方案1一致,只是把缓存和绘制逻辑封装到Painter内部,可直接在Image、Box等组件中使用。

自定义Painter实现示例:

class CachedDrawingPainter : Painter() {
    private var cachedBitmap: ImageBitmap? = null
    private var offscreenCanvas: Canvas? = null
    private val currentPath = Path()
    val paint = Paint().apply {
        color = Color.Black
        strokeWidth = 4f
        style = PaintingStyle.Stroke
        strokeCap = StrokeCap.Round
        isAntiAlias = true
    }

    override val intrinsicSize: Size
        get() = cachedBitmap?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: Size.Unspecified

    override fun DrawScope.onDraw() {
        cachedBitmap?.let { drawImage(it) }
        drawPath(currentPath, paint)
    }

    /**
     * 初始化缓存尺寸,在绘制组件首次测量完成后调用
     */
    fun initCacheSize(width: Int, height: Int) {
        if (cachedBitmap != null) return
        cachedBitmap = ImageBitmap(width, height)
        offscreenCanvas = Canvas(cachedBitmap!!)
        offscreenCanvas?.drawRect(Color.White)
    }

    /**
     * 添加新的绘制路径段
     */
    fun addPathSegment(start: Offset, end: Offset) {
        currentPath.moveTo(start.x, start.y)
        currentPath.lineTo(end.x, end.y)
        offscreenCanvas?.drawPath(currentPath, paint)
        currentPath.reset()
    }

    /**
     * 清空所有绘制内容
     */
    fun clear() {
        cachedBitmap?.let {
            offscreenCanvas = Canvas(it)
            offscreenCanvas?.drawRect(Color.White)
        }
    }
}

扩展功能适配

如果需要支持撤销、重做功能,可在上述方案基础上增加分层缓存逻辑:每累计N条绘制路径就生成一个缓存节点,撤销操作只需要回退到上一个缓存节点,再重绘当前节点的少量路径即可,不会出现全量重绘的性能问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 14:45:04