Issue
This is Q&A-style question since i was looking for a drawing sample with Jetpack Canvas but questions on stackoverflow, this one or another one, i found use pointerInteropFilter
for drawing like View's onTouchEvent
MotionEvent
s which is not advised according to docs as
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.
Solution
We need motion states as we have with View's first
val ACTION_IDLE = 0
val ACTION_DOWN = 1
val ACTION_MOVE = 2
val ACTION_UP = 3
Path, current touch position and touch states
val path = remember { Path() }
var motionEvent by remember { mutableStateOf(ACTION_IDLE) }
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
These are optional for debugging, no need if you don't want to debug
// 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 for creating touch events. Modifier.clipToBounds()
is to prevent drawing outside of 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"
}
}
}
And apply this modifier to canvas and move or draw based on current state and position
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)
// Change state to idle to not draw in wrong position
// if recomposition happens
motionEvent = ACTION_IDLE
}
else -> Unit
}
drawPath(
color = Color.Red,
path = path,
style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round)
)
}
Edit
There is also delay after down should be taken into consideration with awaitFirstDown
and awaitPoiterEvent
. I used 20ms delay with scope.launch{delay(20)} to overcome Canvas missing fast events.
Github repo is here.
Answered By - Thracian
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)