Blog Infos
Author
Published
Topics
Published

Nearly three years have passed since I penned an article on implementing touch hold callbacks in Android, using the old view system. However, the advent of Jetpack Compose has revolutionized how we handle touch events in our apps. This new article will explore the latest approach to achieving the same functionality using Jetpack Compose to create a touch held down Modifier and improve the user experience of your Android app.

We won’t be getting into the touch handling in Compose today. But don’t panic! Check out a droidcon post for a deep dive, and if you’re still lost, the Android Developers docs are here to rescue you!

If you already have my previous implementation of touch held-down in your codebase, you’ll be happy to know that Compose has interop support that lets you use it seamlessly. This means that you can use the pointerInteropFilter() Modifier with your existing code, allowing you to take advantage of Compose’s powerful features without having to start from scratch.

Since Compose doesn’t have a Modifier that provides continuous callbacks when the user holds down their touch, we’ll need to fill this gap by implementing a reusable Modifier.

Please note that this is not a tutorial, but rather a fast-forward answer to get you going forward.

Our main goal is to create UI components that are more interactive and help users take continuous actions, even if those actions need to be accelerated or decelerated over time. To achieve this, developers need to be able to track the elapsed time since a user initially pressed down on a composable, and then take appropriate action based on that information.

To help you imagine the potential end-user experience, here is a basic example that you could build.

Our initial Modifier is the onTouchHeld(), which takes a pollDelay parameter to set the time window between each callback, and a function to invoke with an elapsed duration since the user held down their touch.

fun Modifier.onTouchHeld(
    pollDelay: Duration,
    onTouchHeld: (timeElapsed: Duration) -> Unit
) = composed {
    val scope = rememberCoroutineScope()
    pointerInput(onTouchHeld) {
        awaitEachGesture {
            val initialDown = awaitFirstDown(requireUnconsumed = false)
            val initialDownTime = System.nanoTime()
            val initialTouchHeldJob = scope.launch {
                while (initialDown.pressed) {
                    val timeElapsed = System.nanoTime() - initialDownTime
                    onTouchHeld(timeElapsed.nanoseconds)
                    delay(pollDelay)
                }
            }
            waitForUpOrCancellation()
            initialTouchHeldJob.cancel()
        }
    }
}

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

No results found.

Moving on to our next Modifier is onTouchHeldAnimated(), this time our Modifier takes additional parameters that allow you to increase or decrease the polling delay over a given time and even supports various Easing functions out of the box.

fun Modifier.onTouchHeldAnimated(
    easing: Easing = FastOutSlowInEasing,
    pollDelay: Duration = 500.milliseconds,
    targetPollDelay: Duration = pollDelay,
    animationDuration: Duration = 5.seconds,
    onTouchHeld: () -> Unit
) = composed {
    val scope = rememberCoroutineScope()
    pointerInput(onTouchHeld) {
        val animationSpec: FloatAnimationSpec = FloatTweenSpec(
            animationDuration.inWholeMilliseconds.toInt(),
            0,
            easing
        )
        awaitEachGesture {
            val initialDown = awaitFirstDown(requireUnconsumed = false)
            val initialTouchHeldJob = scope.launch {
                var currentPlayTime = 0.milliseconds
                var delay = pollDelay
                while (initialDown.pressed) {
                    onTouchHeld()
                    delay(delay.inWholeMilliseconds)
                    currentPlayTime += delay
                    delay = animationSpec.getValueFromNanos(
                        currentPlayTime.inWholeNanoseconds,
                        pollDelay.inWholeMilliseconds.toFloat(),
                        targetPollDelay.inWholeMilliseconds.toFloat(),
                        0F
                    ).toInt().milliseconds
                }
            }
            waitForUpOrCancellation()
            initialTouchHeldJob.cancel()
        }
    }
}

To achieve a counter, that accelerates over time, as demonstrated in our example, you can set a lower targetPollDelay. This will progressively reduce the delay between each callback over time, resulting in an accelerated increase in the counter if you are incrementing it on each callback. This approach can help create a dynamic and engaging user experience.

Incorporating new modifiers like onTouchHeld() and onTouchHeldAnimated() can help you create dynamic user experiences in Jetpack Compose. Give them a try and see how they can take your Compose-powered UIs to the next level!

This article was previously published on proandroiddev.com

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