
Tectes
Whether you’re trying to build a painting app or you want to implement a signature pad, drawing on screen has never been easier.
Let’s explore how we can build a simple drawing canvas with Jetpack Compose Canvas and PointerInputScope.
Jetpack Compose Canvas exposes DrawScope which can be used to draw shapes and paths. For example, a component that will draw a red rectangle can be defined as follow: Jetpack Compose Canvas exposes DrawScope which can be used to draw shapes and paths. For example, a component that will draw a red rectangle can be defined as follow:
@Composable
fun DrawingCanvas(
modifier: Modifier = Modifier
) {
Canvas(
modifier = modifier
) {
drawRect(Color.Red)
}
}
PointerInputScope is an API that helps detecting gestures such as tap, press, scroll, drag, swipe, etc. It’s possible to handle gesture at a even lower level by listening to pointer event, but it won’t be necessary for what we want to achieve.
We want to track 2 types of gestures:
We need to record the gestures from the user and draw them on the screen in the canvas. We’re going to use drawPath which takes as main parameter a Path. A path is a list of instructions for drawing. For example, to draw a line:
fun DrawLine(from: Offset, to: Offset): Path {
val path = Path()
path.moveTo(from.x, from.y) // move to a specific coordinate
path.lineTo(to.x, to.y) // draw a line to the destination
return path
}
We’re going to record two types of gestures: detectTapGestures
and detectDragGestures
. Let’s create a view model class that will track and record our gestures as paths:
class DrawingCanvasViewModel: ViewModel() {
val paths = mutableStateListOf()
val currentPath = mutableStateOf(null)
val currentPathRef = mutableIntStateOf(1)
private val lastOffset = mutableStateOf(null)
// MARK: - Tap gestures
fun onTapGesture(offset: Offset) {
currentPath.value = Path()
currentPath.value?.moveTo(offset.x, offset.y)
currentPath.value?.addRect(
Rect(
offset.x - 0.5f,
offset.y - 0.5f,
offset.x + 0.5f,
offset.y + 0.5f
)
)
currentPath.value.let { value ->
if (value != null) {
paths.add(value)
}
}
}
// MARK: - Dragging gestures
fun onDragStart(offset: Offset) {
currentPath.value = Path()
currentPath.value?.moveTo(offset.x, offset.y)
currentPathRef.intValue += 1
lastOffset.value = offset
}
fun onDrag(offset: Offset) {
if (lastOffset.value != null) {
val newOffset = Offset(
lastOffset.value!!.x + offset.x,
lastOffset.value!!.y + offset.y
)
currentPath.value?.lineTo(newOffset.x, newOffset.y)
currentPathRef.intValue += 1
lastOffset.value = newOffset
}
}
fun onDragEnd() {
currentPath.value.let { value ->
if (value != null) {
paths.add(value)
currentPath.value = null
currentPathRef.intValue = 0
}
}
}
fun onDragCancel() {
currentPath.value = null
}
}
A few explanations:
onTapGesture
moves to the position and draw a small rectangle (this is a workaround to handle dots). This represents a Path on its own.onDragStart
creates a new Path and instruct to move to the given positiononDrag
applies the dragging offset to the last recorded dragging position and add this instruction to the recording path.onDragEnd
stores the recording path in the path listonDragCancel
deletes the recording pathcurrentPathRef
is an integer that gets incremented as we’re updating the recording path. This will be observed by the canvas to update the drawing on screen as the input updatesHere’s what the composable looks like
@Composable
fun DrawingCanvas(
modifier: Modifier = Modifier,
vm: DrawingCanvasViewModel
) {
Canvas(
modifier = modifier
.pointerInput(Unit) {
detectTapGestures { vm.onTapGesture(it) }
}
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { vm.onDragStart(it) },
onDragEnd = { vm.onDragEnd() },
onDragCancel = { vm.onDragCancel() },
onDrag = { _, amount -> vm.onDrag(amount) }
)
}
) {
vm.paths.forEach { drawPath(path = it, color = Color.Black, style = Stroke(10f)) }
if (vm.currentPath.value != null && vm.currentPathRef.intValue > 0) {
drawPath(path = vm.currentPath.value!!, color = Color.Black, style = Stroke(10f))
}
}
}
Note that the DrawScope
has two instructions:
There are many things that can be do to improve this code: