Blog Infos
Author
Published
Topics
,
Published

I work on a real estate app that was written in Kotlin with XML with many custom animations and views. A few months ago, we decided to migrate our application to Jetpack Compose. In our real estate app, we have a unique functionality that allows users to draw specific areas on the map, restricting ads to only those within the selected region. In the previous version of the app, we utilized a Custom View to draw polygons and then transformed screen coordinates to map coordinates.

In this post, I’ll demonstrate how we implemented this feature in Jetpack Compose. It will be a simple view that only can draw lines and nothing else, we will Canvas composable for this.

@Composable
fun MapDrawer() {
    Canvas(
        modifier = Modifier.fillMaxSize(),
        onDraw = {
            
        }
    )
}

onDraw – lambda that will be called to perform drawing

In onDraw lambda we can call various method to draw. There are some of them:

drawPath()
drawLine()
drawArc()
drawCircle()
drawRect()

As you can see it’s pretty similar to Canvas that is used in XML based views.

You can read more about drawing:

Graphics in Compose | Jetpack Compose | Android Developers

Many apps need to be able to precisely control exactly what’s drawn on the screen. This might be as small as putting a…

developer.android.com

For now, our attention won’t be spread across all features. Instead, we’ll exclusively focus on the implementation of drawPath in our example.

The next step involves dealing with touch events within our Canvas composable. To address this, we’ll leverage the pointerInput modifier, providing us with the capability to effectively handle all events taking place within our composable.

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
): Modifier = this then SuspendPointerInputElement(
    key1 = key1,
    pointerInputHandler = block
)

It receives only 2 parameters: key and block. For key we will just pass Unit. So we have something like this

@Composable
fun MapDrawer() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit){
                               
            },
        onDraw = {

        }
    )
}

To actively handle events, we need to invoke the awaitEachGesture function. This is a suspend function that runs in a loop while the composable is active. Within this function, we use awaitPointerEvent to manage the current event. It’s essential to call this function inside a while loop to continuously receive events.

So we will have something like this:

@Composable
fun MapDrawer() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitEachGesture {
                    do {
                        val event: PointerEvent = awaitPointerEvent()
                    } while (event.changes.any { it.pressed })
                }
            },
        onDraw = {}
    )
}

Our event variable contains lots of information about happened event. But we will use changes property to get information that we need to draw.

@Composable
fun MapDrawer() {
    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitEachGesture {
                    do {
                        val event: PointerEvent = awaitPointerEvent()
                        event.changes.forEach { changes ->
                            // contains coordinates where event happened
                            val position = changes.position
                        }
                    } while (event.changes.any { it.pressed })
                }
            },
        onDraw = {}
    )
}

Now let’s create a state object which will contains information about touch event.

enum class DrawerMotionEvent { idle, down, up, move }

data class MapPolygonState(
    val currentPosition: Offset = Offset.Unspecified,
    val event: MotionEvent = MotionEvent.idle
)

Let’s update our composable:

@Composable
fun MapDrawer() {
    val path = remember { Path() }
    var state by remember { mutableStateOf(MapPolygonState()) }
    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitEachGesture {
                    do {
                        val event: PointerEvent = awaitPointerEvent()
                        event.changes.forEach { changes ->
                            // Updates state with new event and position
                            state = state.copy(
                                currentPosition = changes.position,
                                event = DrawerMotionEvent.move
                            )
                        }
                    } while (event.changes.any { it.pressed })
                }
            },
        onDraw = {}
    )
}

In the code, we update our state object every time when new event handled, which will initiate recomposition of our composable on every touch. We also have path variable here which will holds all points.

Now let’s jump on to onDraw block. Here we will add points to our path and draw it on canvas. We just need to draw our path

onDraw = {
    when (state.event) {
        DrawerMotionEvent.idle -> Unit
        DrawerMotionEvent.up, DrawerMotionEvent.move -> path.lineTo(
            state.currentPosition.x,
            state.currentPosition.y
        )
        DrawerMotionEvent.down -> path.moveTo(state.currentPosition.x, state.currentPosition.y)
    }
    drawPath(
        path = path,
        brush = brush,
        style = Stroke(width = 5f)
    )
}

Our composable will looks like this:

enum class DrawerMotionEvent { idle, down, up, move }
data class MapDrawState(
val currentPosition: Offset = Offset.Unspecified,
val event: MotionEvent = MotionEvent.idle
)
@Composable
fun MapDrawer() {
var state by remember { mutableStateOf(MapDrawState()) }
val brush = remember { SolidColor(Color.Black) }
val path = remember { Path() }
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture {
do {
val event: PointerEvent = awaitPointerEvent()
event.changes.forEach { changes ->
val position = changes.position
state = state.copy(
currentPosition = position,
event = DrawerMotionEvent.move
)
}
} while (event.changes.any { it.pressed })
// Touch Up event
state = state.copy(
currentPosition = currentEvent.changes.first().position,
event = DrawerMotionEvent.up
)
}
},
onDraw = {
when (state.event) {
DrawerMotionEvent.idle -> Unit
DrawerMotionEvent.up, DrawerMotionEvent.move -> path.lineTo(
state.currentPosition.x,
state.currentPosition.y
)
DrawerMotionEvent.down -> path.moveTo(state.currentPosition.x, state.currentPosition.y)
}
drawPath(
path = path,
brush = brush,
style = Stroke(width = 5f)
)
}
)
}
view raw MapDrawer.kt hosted with ❤ by GitHub

And here is the example:

As you can see we have straight line drawn from top left corner. That’s because Path always start from (0,0) point. To fix this we can add this in our awaitEachGesture function before our loop block

awaitPointerEvent().changes.first().also { changes ->
    val position = changes.position
    state = state.copy(
        currentPosition = position,
        event = DrawerMotionEvent.down
    )
}

By adding this before entering the loop, we ensure that the starting point of the path corresponds to the initial touch position. This adjustment sets the path to begin at the touch point, providing a more intuitive drawing experience.

So on our first touch we will move our path to correct location. Now let’s try again

Yay, we draw in Jetpack Compose!

Here is the full code of Composable:

enum class DrawerMotionEvent { idle, down, up, move }
data class MapDrawState(
val currentPosition: Offset = Offset.Unspecified,
val event: MotionEvent = MotionEvent.idle
)
@Composable
fun MapDrawer() {
var state by remember { mutableStateOf(MapDrawState()) }
val brush = remember { SolidColor(Color.Black) }
val path = remember { Path() }
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture {
awaitPointerEvent().changes.first().also { changes ->
val position = changes.position
state = state.copy(
currentPosition = position,
event = DrawerMotionEvent.down
)
}
do {
val event: PointerEvent = awaitPointerEvent()
event.changes.forEach { changes ->
val position = changes.position
state = state.copy(
currentPosition = position,
event = DrawerMotionEvent.move
)
}
} while (event.changes.any { it.pressed })
// Touch Up event
state = state.copy(
currentPosition = currentEvent.changes.first().position,
event = DrawerMotionEvent.up
)
}
},
onDraw = {
when (state.event) {
DrawerMotionEvent.idle -> Unit
DrawerMotionEvent.up, DrawerMotionEvent.move -> path.lineTo(
state.currentPosition.x,
state.currentPosition.y
)
DrawerMotionEvent.down -> path.moveTo(state.currentPosition.x, state.currentPosition.y)
}
drawPath(
path = path,
brush = brush,
style = Stroke(width = 5f)
)
}
)
}

That wraps up the initial part of our tutorial. Today, we covered the fundamentals of drawing on the Canvas using its methods and also handling touch events in Jetpack Compose.

In the next part our journey will shift towards the transformation of our drawings into Google Maps polygons and extracting their real coordinate bounds.

Part 2:

Free hand draw polygon in Google Maps Compose. Part 2

In the previous part we learned how to draw in Jetpack Compose using Canvas API. If you didn’t read it yet, here is the…

agarasul.medium.com

Feel free to follow me on Twitter and don’t hesitate to ask questions related to Jetpack Compose.

Twitter: https://twitter.com/a_rasul98

Also check out my other post related to Jetpack Compose:

Rasul Aghakishiyev – Medium

Read writing from Rasul Aghakishiyev on Medium. Android Software Engineer. Interested in mobile development. In love…

agarasul.medium.com

Thanks for reading and see you later!

This article was previously published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu