如何使用触摸事件在 Jetpack Compose Canvas 上绘图?

How to draw on Jetpack Compose Canvas using touch events?

这是 Q&A-style question since i was looking for a drawing sample with Jetpack Canvas but questions on Whosebug, this one or another one,我发现使用 pointerInteropFilter 进行绘图,如 View 的 onTouchEvent MotionEvents 根据文档不建议这样做

A special PointerInputModifier that provides access to the underlying MotionEvents originally dispatched to Compose. Prefer pointerInput and use this only for interoperation with existing code that consumes MotionEvents.

While the main intent of this Modifier is to allow arbitrary code to access the original MotionEvent dispatched to Compose, for completeness, analogs are provided to allow arbitrary code to interact with the system as if it were an Android View.

我们需要 View 的第一个运动状态

val ACTION_IDLE = 0
val ACTION_DOWN = 1
val ACTION_MOVE = 2
val ACTION_UP = 3

路径、当前触摸位置和触摸状态

val path = remember { Path() }
var motionEvent by remember { mutableStateOf(ACTION_IDLE) }
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }

调试时可选,不想调试的不需要

// color and text are for debugging and observing state changes and position
var gestureColor by remember { mutableStateOf(Color.LightGray) }
var gestureText by remember { mutableStateOf("Touch to Draw") }

用于创建触摸事件的修饰符。 Modifier.clipToBounds() 是为了防止在 Canvas.

之外绘制
val drawModifier = Modifier
    .fillMaxWidth()
    .height(400.dp)
    .background(gestureColor)
    .clipToBounds()
    .pointerInput(Unit) {
        forEachGesture {
            awaitPointerEventScope {

                // Wait for at least one pointer to press down, and set first contact position
                val down: PointerInputChange = awaitFirstDown().also {
                    motionEvent = ACTION_DOWN
                    currentPosition = it.position
                    gestureColor = Blue400
                }


                do {
                    // This PointerEvent contains details including events, id, position and more
                    val event: PointerEvent = awaitPointerEvent()

                    var eventChanges =
                        "DOWN changedToDown: ${down.changedToDown()} changedUp: ${down.changedToUp()}\n"
                    event.changes
                        .forEachIndexed { index: Int, pointerInputChange: PointerInputChange ->
                            eventChanges += "Index: $index, id: ${pointerInputChange.id}, " +
                                    "changedUp: ${pointerInputChange.changedToUp()}" +
                                    "pos: ${pointerInputChange.position}\n"

                            // This necessary to prevent other gestures or scrolling
                            // when at least one pointer is down on canvas to draw
                            pointerInputChange.consumePositionChange()
                        }

                    gestureText = "EVENT changes size ${event.changes.size}\n" + eventChanges

                    gestureColor = Green400
                    motionEvent = ACTION_MOVE
                    currentPosition = event.changes.first().position
                } while (event.changes.any { it.pressed })

                motionEvent = ACTION_UP
                gestureColor = Color.LightGray

                gestureText += "UP changedToDown: ${down.changedToDown()} " +
                        "changedUp: ${down.changedToUp()}\n"
            }
        }
    }

并将此修改器应用于 canvas 并根据当前状态和位置移动或绘制

Canvas(modifier = drawModifier) {

    when (motionEvent) {
        ACTION_DOWN -> {
            path.moveTo(currentPosition.x, currentPosition.y)
        }
        ACTION_MOVE -> {

            if (currentPosition != Offset.Unspecified) {
                path.lineTo(currentPosition.x, currentPosition.y)
            }
        }

        ACTION_UP -> {
            path.lineTo(currentPosition.x, currentPosition.y)
        }

        else -> Unit
    }

    drawPath(
        color = Color.Red,
        path = path,
        style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round)
    )
}

编辑

awaitFirstDownawaitPoiterEvent也应考虑down后的延迟。我使用 scope.launch{delay(20)} 的 20 毫秒延迟来克服 Canvas 丢失的快速事件。

Github 回购是 here.