Collapsing Toolbar in Jetpack Compose | ‘LazyColumn’ version — Part 2
A collapsing toolbar mechanism built completely in Jetpack Compose.
You can go to any article of this guide by clicking on one of the links below:
Collapsing Toolbar in Jetpack Compose
⚠️ ATTENTION ⚠️
💬 If you want to skip descriptions, comments and explanations, follow this symbol 🔷 to go from instruction to instruction.
💬 Stop when you see this symbol 🔶 to check if you have followed the instructions correctly by comparing your code against the one I provide at that point.
💬 If you see this symbol ▶️, you can run the application to check your progress.
💬 If you still haven’t downloaded the codebase, wait no more and click on the following link:
In the previous article we ended up with a working collapsing toolbar, although there’s still one thing left to do in order to complete our collapsing toolbar functionality. I asked you to run the application, scroll down as much as you can, and perform a fling gesture to scroll all the way up quickly. The result of that operation was a lack of reaction by the toolbar once the top of the list has been reached.
To solve that, we need to implement the onPostFling
callback of the NestedScrollConnection
object.
🔷 Open the file Catalog.kt
and add the following onPostFling
callback implementation code inside the NestedScrollConnection
object:
While onPreScroll
is called before anything else in the nested scroll chain, onPostFling
is called last. All we need to do is consume what’s left in the available
parameter in the exact same way that onPreScroll
does.
▶️ You can run the application just to see how it behaves now.
The toolbar is now able to react to a fling gesture, but there is a bad thing about this implementation: the toolbar enters abruptly with no animation. Noticing it depends on two things: how picky you are, and how fast you fling. The slower the fling, the more noticeable it becomes.
Let’s fix it with one of the best and powerful things that Jetpack Compose provides: its Animation APIs. If you take a look at the official documentation, you’ll find out that there are many different types of animations that can be applied depending on the use case. There is a special one for our specific case called DecayAnimationSpec.
First things first. We need a coroutine scope to run the animation in the scope of a coroutine, so we can stop/cancel the animation whenever it’s necessary.
🔷 Open the file Catalog.kt
and find the Catalog
composable function.
🔷 Add the following line of code before the declaration of the variable nestedScrollConnection
:
🔷 Replace the onPostFling
callback implementation with the following code:
We’re restricting its reaction only to a fling gesture whose scroll direction is upwards: available.y > 0
. Inside the if
block, we start a new coroutine to execute the corresponding animation. And lastly, with a call to the animateDecay
function, we start the animation that corresponds to our specific use case.
The animateDecay
function has three input parameters and a trailing lambda:
initialValue
: The current height of the toolbar plus its offset.initialVelocity
: This value is provided in theavailable
parameter ofonPostFling
.animationSpec
: This parameter requires an implementation of theFloatDecayAnimationSpec
interface.block
: This is a trailing lambda that gets invoked on each animation frame with up-to-date value and velocity.
What defines how the animation behaves is the animationSpec
parameter. There are two built-in implementations of the interface FloatDecayAnimationSpec
:
- SplineBasedFloatDecayAnimationSpec: A native Android fling curve decay.
- FloatExponentialDecaySpec: This is a decay animation where the friction/deceleration is always proportional to the velocity. As a result, the velocity goes under an exponential decay. The constructor parameter,
frictionMultiplier
, can be tuned to adjust the amount of friction applied in the decay. The higher the multiplier, the higher the friction, the sooner the animation will stop, and the shorter distance the animation will travel with the same starting condition.
💬 Both descriptions have been taken from their respective official documentation.
And of course, in case you’re not satisfied with either of these implementations, you can always provide your own one.
📖 You can read this article written by Elye which illustrates the behavior of both implementations and shows how to create a custom one.
We’re close to finish. There is something that we must not ignore now that we have included coroutines and animations into “the equation”. Since this is an animation that doesn’t stop its execution until its current velocity is 0
, we need to stop/cancel it before we start scrolling again, this way we prevent an erratic behavior.
💬 If you want to know what I’m talking about, ▶️ you can run the application and try to scroll down before the toolbar gets completely expanded after a fling gesture.
We need to stop the animation as soon as we tap on the list. We can achieve this by providing a tap gesture detector via the pointerInput
modifier and calling scope.coroutineContext.cancelChildren()
inside the onPress
lambda:
🔷 Include the pointerInput
modifier to LazyCatalog
with the following implementation:
.pointerInput(Unit) {
detectTapGestures(
onPress = { scope.coroutineContext.cancelChildren() }
)
}
🔶 The complete code inside Catalog.kt
should look like this:
▶️ You can run the application to check the final result.
We’re done! Now we have a collapsing toolbar, implemented along with a LazyColumn
, that responds to fling gestures properly.
💬 If you enjoyed this article, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.
- 📖 You can find the official documentation of NestedScrollConnection at https://developer.android.com/reference/kotlin/androidx/compose/ui/input/nestedscroll/package-summary.
- 📖 You can find the official documentation about RememberCoroutineScope at https://developer.android.com/jetpack/compose/side-effects#remembercoroutinescope.
- 📖 You can find the official documentation about gestures detection, especifically Tapping and pressing, at https://developer.android.com/jetpack/compose/gestures#tapping.