Collapsing Toolbar in Jetpack Compose | ‘LazyColumn’ version — Part 2

Glenn Sandoval
Kotlin and Kotlin for Android
5 min readMay 30, 2022

--

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

  1. Problem, solutions and alternatives
  2. Codebase
  3. ‘Column’ version
  4. ‘LazyColumn’ version — Part 1
  5. ‘LazyColumn’ version — Part 2
  6. A closer look at the Toolbar — Part 1
  7. A closer look at the Toolbar — Part 2

⚠️ 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 the available parameter of onPostFling.
  • animationSpec: This parameter requires an implementation of the FloatDecayAnimationSpec 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.

--

--

Glenn Sandoval
Kotlin and Kotlin for Android

I’m a software developer who loves learning and making new things all the time. I especially like mobile technology.