如何使用触摸事件在 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
MotionEvent
s 根据文档不建议这样做
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)
)
}
编辑
awaitFirstDown
和awaitPoiterEvent
也应考虑down后的延迟。我使用 scope.launch{delay(20)} 的 20 毫秒延迟来克服 Canvas 丢失的快速事件。
Github 回购是 here.
这是 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
MotionEvent
s 根据文档不建议这样做
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)
)
}
编辑
awaitFirstDown
和awaitPoiterEvent
也应考虑down后的延迟。我使用 scope.launch{delay(20)} 的 20 毫秒延迟来克服 Canvas 丢失的快速事件。
Github 回购是 here.