How to draw on Jetpack Compose Canvas using touch events?
Asked Answered
P

1

11

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 MotionEvents 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.

Pasha answered 12/2, 2022 at 8:17 Comment(0)
P
22

Edit

It's been a while since i posted this answer and as i got feedback from this question that previous answer was a little bit confusing for beginners, so i simplify it, library for this gesture and more is available in github repo.

We need motion states as we have with View's first

enum class MotionEvent {
    Idle, Down, Move, Up
}

Idle state is needed to not leave state on Up because if any recomposition happens your Canvas gets recomposed with Up state which leads to unwanted drawings or even crashes.

Path, current touch position and touch states

var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }

previousPosition is optional i use it because i want to draw smooth lines with path.quadraticBezierTo, instead of path.lineTo while moving with pointer

Modifier for creating touch events. Modifier.clipToBounds() is to prevent drawing outside of Canvas.

val drawModifier = Modifier
    .fillMaxWidth()
    .height(300.dp)
    .clipToBounds()
    .background(Color.White)
    .pointerMotionEvents(
        onDown = { pointerInputChange: PointerInputChange ->
            currentPosition = pointerInputChange.position
            motionEvent = MotionEvent.Down
            pointerInputChange.consume()
        },
        onMove = { pointerInputChange: PointerInputChange ->
            currentPosition = pointerInputChange.position
            motionEvent = MotionEvent.Move
            pointerInputChange.consume()
        },
        onUp = { pointerInputChange: PointerInputChange ->
            motionEvent = MotionEvent.Up
            pointerInputChange.consume()
        },
        delayAfterDownInMillis = 25L
    )

Modifier.pointerMotionEvents custom gesture library i wrote for it be counterpart of onTouchEvent, it's available on github repo above, and here is a detailed explanation about gestures, you can easily build your own gesture if you don't want to. Delay after first touch occurs on onTouchEvent of View has, it's about 16ms on my devices, this is the fastest i measured, i added to gestures on Compose too because Canvas can't process down events when user has a very swift pointer movement initially.

And apply this modifier to canvas and move or draw based on current state and position

Canvas(modifier = drawModifier) {


    when (motionEvent) {
        MotionEvent.Down -> {
            path.moveTo(currentPosition.x, currentPosition.y)
            previousPosition = currentPosition
        }

        MotionEvent.Move -> {
            path.quadraticBezierTo(
                previousPosition.x,
                previousPosition.y,
                (previousPosition.x + currentPosition.x) / 2,
                (previousPosition.y + currentPosition.y) / 2

            )
            previousPosition = currentPosition
        }

        MotionEvent.Up -> {
            path.lineTo(currentPosition.x, currentPosition.y)
            currentPosition = Offset.Unspecified
            previousPosition = currentPosition
            motionEvent = MotionEvent.Idle
        }

        else -> Unit
    }

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

enter image description here

Github repo for a full drawing app also available here.

Pasha answered 12/2, 2022 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.