Jetpack Compose: Detect the Number of Fingers Touching the Screen
Jetpack Compose

Jetpack Compose: Detect the Number of Fingers Touching the Screen

2024-03-20
10 min read

Jetpack Compose: Detect the current number of touches on the screen

Jetpack Compose Logo

On my journey learning Jetpack Compose and I had the need to come up with a solution to detecting the number of fingers touching the screen. Here are two approaches I took to solve this.

There are many ways to detect gestures of multiple touches in the Jetpack Compose gestures documentation, but none give you a way to get the exact number of fingers that touched the screen.

https://developer.android.com/jetpack/compose/gestures

The classic approach prior to Jetpack Compose is to get the pointer count from a MotionEvent by overriding OnTouchEvent:

https://developer.android.com/training/gestures/detector

So I decided to research a way to detect gestures in Jetpack compose that would give me a MotionEvent.

I stumbled upon PointerInteropFilter after a quick google search. It’s a Jetpack Compose Modifier.

Here is my first solution:

@ExperimentalComposeUiApi
@Composable
fun Greeting(name: String) {
    *Card*(modifier = Modifier
            .*fillMaxSize*()
            .*pointerInteropFilter ***{ **motionEvent **->
                **when (motionEvent.*action *and MotionEvent.*ACTION_MASK *) {
                    MotionEvent.*ACTION_POINTER_DOWN *-> {
                        val pointerCount = motionEvent.*pointerCount
                        *Log.d("Greeting", "number of fingers detected: $pointerCount")
                    }
                }
                true
            **}
            **, content = **{ ***Text*(text = name) **}**)
}

Greeting is a Composable that is automatically generated by Android Studio when creating a blank Jetpack Compose app. I changed it to use a Card a composable. The fillMaxSize() Modifier makes the card fill the entire view of the screen. I did this for convenience so I can swipe anywhere.

motionEvent.*action *and MotionEvent.*ACTION_MASK*

The above line is a common practice when detecting multi-touch in Android. Check here for more info on this: Android multitouch and getActionMasked()

This is a working solution! But it does come with some downsides depending on your use case. It continues to listen to touches. I wanted a way to stop listening after I have detected my desired number of touches on the screen. Luckily Android is open source, so I decided to jump in a see what I could find.

I decided to first look at the documentation and implementation of PointerInteropFilter first:

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilter.android.kt

It only exposes so much so I started to explore other options before continuing this route. I thought of the Jetpack Compose gesture documentation I mentioned earlier and how they gave you ways to detect multi-touch. So I started to look at the documentation again and noticed they all used the PointerInput Modifier.

https://developer.android.com/jetpack/compose/gestures

So I started to look into the gestures implementations:

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/

I noticed they all use the ForEachGesture:

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt

After looking at that code and examples in the kotlin-lang workspace. I was able to come up with this second solution.

@ExperimentalComposeUiApi
@Composable
fun Greeting(name: String) {
    *Card*(modifier = Modifier
        .*fillMaxSize*()
        .*pointerInput*(Unit) **{
            **val fingerCount = 2;
            forEachGesture **{
                **val context = currentCoroutineContext()
                awaitPointerEventScope **{
                    **do {
                        val event = awaitPointerEvent()
                        if (event.changes.size == fingerCount) {
                            context.*cancel*()
                        }
                    } while (event.changes.*any ***{ it**.pressed **} **&& context.isActive)
                **}
            }
        }**, content = **{ ***Text*(text = name) **}**)
}

You will need to keep getting the size from the list of changes of the event. I also needed the context from forEachGesture so that I can stop processing input. You will need the coroutines-android library. Because of some issues, I ran into, I had to use this one at the time of writing this(https://stackoverflow.com/a/70231016):

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"

I thought of doing this after seeing this comment in the ForEachGesture source code:

/**

* Repeatedly calls [block] to handle gestures. If there is a [CancellationException],

* it will wait until all pointers are raised before another gesture is detected, or it

* exits if [isActive] is `false`.

*/

Note you can also look at the source code inside of Android Studio using Command+b ( Mac ) or Control+b ( Windows ) after putting your cursor on the code you want to go to. You can also do this by holding down Command/Control + clicking on the code.

This is as far I got with this. You can create a more robust solution by altering the source code as well.

There is also this great article that also helped tremendously: Using Jetpack Compose to complete custom gesture processing

Thank you for taking the time to read this!