avatarElye - A One Eye Developer

Summary

This article provides a tutorial on how to create a simple drawing app using Android Jetpack Compose, focusing on touch detection and drawing on Canvas.

Abstract

The article discusses the development of a simple drawing app using Android Jetpack Compose. It highlights the three main components required for a drawing app: a Canvas, touch detection, and drawing paths based on touch. The article explains how to set up the Canvas and detect touch using the pointerInteropFilter modifier. It also covers the difference between touch detection and drawing locations, demonstrating how to update a mutableState value to trigger drawing. The article provides a complete code example for the drawing app and discusses a caveat related to missed action updates.

Bullet points

  • The tutorial teaches how to create a simple drawing app using Android Jetpack Compose.
  • The three main components of a drawing app are: a Canvas, touch detection, and drawing paths based on touch.
  • Jetpack Compose uses the pointerInteropFilter modifier to detect touch instead of the conventional onTouchEvent function.
  • Touch detection and drawing locations are different, so a mutableState value is used to trigger drawing.
  • The article provides a complete code example for the drawing app.
  • A caveat is discussed regarding missed action updates in the drawing function.

Learning Android Development

Code Simple Android Jetpack Compose Drawing App

Detect touch to draw on Canvas using Jetpack Compose

Photo by Neven Krcmarek on Unsplash

If you plan to learn Android, the most fun app to start is to make a simple drawing app. Instead of learning the conventional way of drawing, let’s learn using the latest Android Jetpack Compose to do so.

Pre-requisite of Setting Jetpack Compose

As Jetpack Compose is still in Alpha release, to use it, you need to download Android Studio 4.2 (Canary version) and do the setup as below

Drawing in Jetpack Compose

To have a drawing app, we just need three things

  1. A Canvas for one to draw onto
  2. The touch detection (touch down, and touch move)
  3. Drawing the path based on touch detection.

Setting up the Canvas

Unlike the conventional Android Development, it no longer uses layout. Hence we no longer need to build a custom view and have it drawn onto the Canvas.

Instead, we do have a Canvas Compose function.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
         Canvas(modifier = Modifier.fillMaxSize()) {
            // Drawing happens here
        }
    }
}

Here we set up the Modifier with fillMaxSize(), so that it used up the entire space of the app.

Detection of touch

To detect touch, conventionally in Android, in the custom view, we override the onTouchEvent function.

override fun onTouchEvent(event: MotionEvent?): Boolean {
    when (event?.action) {
        MotionEvent.ACTION_DOWN -> { }
        MotionEvent.ACTION_MOVE -> { }
        MotionEvent.ACTION_UP -> { }
        else -> return false
    }
    invalidate()
    return true
}

In Jetpack Compose, we use the pointerInteropFilde>ter modifier to detect the touch.

Canvas(modifier = Modifier
        .fillMaxSize()
        .pointerInteropFilter {
            when (it.action) {
                MotionEvent.ACTION_DOWN -> { }
                MotionEvent.ACTION_MOVE -> { }
                MotionEvent.ACTION_UP -> { }
                else -> false
            }
            true
        }
)

From the codes above, both look similar, with the exception there is no invalidate needed. The way Jetpack Compose works to notify the drawing needed is using some state value change.

Check out below for some comparison of the conventional Android vs Jetpack Compose way of working

Drawing of path based on touch

The location that detects the touch and the drawing is different, as illustrated in the diagram below.

So in order to trigger the drawing, we need to have a mutableState value that behaves like invalidate of the conventional way. Besides, we also need the path to store all the coordinates.

private val action: MutableState<Any?> = mutableStateOf(null)
private val path = Path()

Then upon detection of the drawing, we can update the action and the path as shown below.

when (it.action) {
    MotionEvent.ACTION_DOWN -> {
        action.value = it
        path.moveTo(it.x, it.y)
    }
    MotionEvent.ACTION_MOVE -> {
        action.value = it
        path.lineTo(it.x, it.y)
    }
    else -> false
}

With the action updated, then the drawing will be triggered if it has access to the action as shown below.

{
    action.value?.let {
        drawPath(
                path = path,
                color = Color.Green,
                alpha = 1f,
                style = Stroke(10f))

    }
}

The entire code

In less than 50 lines of code, you have an Android Drawing App made using Jetpack Compose.

private val action: MutableState<Any?> = mutableStateOf(null)
private val path = Path()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        Canvas(modifier = Modifier
                .fillMaxSize()
                .pointerInteropFilter {
                    when (it.action) {
                        MotionEvent.ACTION_DOWN -> {
                            action.value = it
                            path.moveTo(it.x, it.y)
                        }
                        MotionEvent.ACTION_MOVE -> {
                            action.value = it
                            path.lineTo(it.x, it.y)
                        }
                        else -> false
                    }
                    true
                }
        ) {
            action.value?.let {
                drawPath(
                        path = path,
                        color = Color.Green,
                        alpha = 1f,
                        style = Stroke(10f))

            }
        }
    }
}

Caveat

If you are a seasoned developer and look at the code above, you might wonder if we can instead send the coordinate through the action and have the path coordinate set up in the drawing function, like something below

Yes, technically this is permissible.

However based on my experiment, some action update might be missed sending to the drawing function if the subsequent action is triggered (i.e. in particular right after ACTION_DOWN the ACTION_MOVE triggered the action, the action sent from ACTION_DOWN is lost.

Not sure if this is a real limitation of as it is still alpha and not stable, causing the missing action mutable state.

So in order to save guard and get all the path information, let the path setup during the touch detection function, so none is lost.

You can get the code here. Cheers!

Programming
Android App Development
Mobile App Development
AndroidDev
Jetpack Compose
Recommended from ReadMedium