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() } } }
Job Offers
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