如何在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
相关产品推荐
相关产品推荐

