Collapsing Toolbar in Jetpack Compose | ‘Column’ version

Glenn Sandoval
Kotlin and Kotlin for Android
13 min readMay 16, 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:

Let’s get started

First things first. Let’s build a new composable that will work as the initial screen which joins the CollapsingToolbar and EagerCatalog composables:

🔷 Create a new package named screens inside the ui folder.
🔷 Create a new Kotlin file named Catalog.kt inside the screens package.

— Step 1 : New Screen

We need to define two heights for the collapsed and expanded states of the toolbar.

🔷 Inside Catalog.kt, declare both dimensions as follows:

Now with its height range declared, we can create the new screen.

🔷 Create a new composable function named Catalog with the following input parameters:

  • animals: List of animals used for populating the EagerColumn composable.
  • columns: Number of items that will be displayed per row.
  • onPrivacyTipButtonClicked: Lambda that defines the event to be triggered when the ‘Privacy Tip’ button is clicked.
  • onSettingsButtonClicked: Lambda that defines the event to be triggered when the ‘Settings’ button is clicked.
  • modifier: Optional parameter applied to the main structure which joins the CollapsingToolbar and EagerCatalog composables.

The Catalog composable function should look like this:

Leave this function with a TODO() statement for now. Let’s make a necessary change to be able to run the application and see what’s going on when we have written new code.

🔷 Open the file MainActivity.kt and replace the code of the composable function CollapsingToolbarInComposeApp with the following:

Before we run the application, let’s add all the composables that we need in the body of the composable function Catalog. We are going to use a Box as container for the CollapsingToolbar and the EagerCatalog composables.

🔷 Open the file Catalog.kt.
🔷 Copy the body of Catalog below and replace the TODO() statement with it.

▶️ Now you can run the application. This is what we have so far:

Step 1

There are two things that must be changed to make the toolbar collapsible: its parameter progress —line 24— and its modifier property height —line 29 — . Neither should have a fixed value. For its offset, we are going to call its modifier function graphicsLayer, and set its translationY value.

Since progress, height and offset are interrelated properties that depend on the ScrollState object, we are going to follow what the API Guidelines for Jetpack Compose suggests under the Compose API design patterns section, encapsulating all the data involved in the toolbar state management, in favor of its internal consistency.

— Step 2 : Abstraction

We need an interface to manage the toolbar state, not only to encapsulate all the data involved, as I just mentioned, but to establish a contract about the behavior that every scroll flag implementation must provide, in order to make them interchangeable.

First, let’s organize all the code related to the toolbar state management that we are going to write, inside its own package.

🔷 Create a chain of packages that follows the structure management.states.toolbar inside the ui folder.
🔷 Create an interface named ToolbarState inside the toolbar package and add the following code:

Now every scroll flag that we decide to support will be an implementation of ToolbarState, so we can rely on this interface to set the toolbar state values accordingly. Also, since every scroll flag implementation will be in charge of holding its corresponding toolbar state, its state should be able of being restored in case of a configuration change. So, let’s create a function that not only returns a scroll flag implementation, but also preserves its internal state.

🔷 Open the file Catalog.kt and add the following code:

💬 That TODO() statement will be the last thing we are going to change after we’re done coding any scroll flag implementation. It will also be the only line of code that we will need to modify for applying any behavior that we have implemented.

This function receives an IntRange corresponding to the toolbar height range in pixels for its collapsed and expanded states. Every scroll flag implementation will require that range in order to establish the toolbar height limits internally.

🔷 Add the following code inside the composable function Catalog:

🔷 Update the toolbar state as follows:

🔷 Replace all the fixed parameters of CollpasingToolbar that we set before, with the ones provided by whichever scroll flag implementation we’re using:

🔶 At this point, the code inside Catalog.kt should look like this:

It’s time to work on the different ToolbarState implementations.

— Step 3 : Common implementation

All four different scroll flags share some of their behavior, so we can approach their implementations encapsulating their common member variables and functions in a base abstract class.

🔷 Create a new abstract class named ScrollFlagState inside the toolbar package and make it implement the ToolbarState interface.

To establish its height range, this class should receive that information as parameter. Also, since every toolbar state should be able of being restored, its scroll value should be received as parameter.

🔷 Add these two parameters to its constructor: heightRange: IntRange and scrollValue: Int.

🔷 Validate heightRange with a require statement inside an init block.

🔷 Add the following member variables for internal control:

🔷 Override its progress property making use of its custom getter with the following calculation:

🔶 And that’s all for this class. Its complete code should look like this:

— Step 4 : A little more abstraction

Now we have a common implementation, but we can take this abstraction a little further. There are two scroll flags that have something in common. I’m talking about the EnterAlways and EnterAlwaysCollapsed scroll flags. Both scroll flags make the toolbar leave or enter the screen independently of the scroll value/position. To translate this feature into code, we need to create another abstract class which extends the previous one.

🔷 Create a new abstract class named DynamicOffsetScrollFlagState inside the toolbar package and make it a subclass of ScrollFlagState:

This way, we can calculate an offset independently of the scroll value/position.

We finally reached the last step where we are going to code every concrete scroll flag implementation. Here is where all this abstraction makes sense, so you’ll see how easy implementing a new behavior can be.

— Step 5 : Concrete implementations

🔷 Create a new package named scrollflags inside the toolbar folder.

We are going to save every concrete scroll flag implementation inside this new package.

🔷 Continue by clicking on the scroll flag you’re interested in:

ScrollState

🔷 Create a new class named ScrollState and make it a subclass of ScrollFlagState:

🔷 Override its scrollValue property as follows:

This one is pretty straightforward. All it does is update its member variable _scrollValue, which works as a kind of backing property.

💬 0 is the initial value of scrollValue. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than 0.

🔷 Override its height property as follows:

💬 rangeDifference = maxHeight — minHeight.

height takes a value between minHeight and maxHeight. When scrollValue is 0, height is equal to maxHeight, which means that the toolbar is expanded. When scrollValue is ≥ rangeDifference, height is equal to minHeight, which means that the toolbar is collapsed.

🔷 Override its offset property as follows:

💬 rangeDifference = maxHeight — minHeight.

offset takes a value between 0 and -minHeight. When scrollValue is ≤ rangeDifference, offset is 0, which means that the toolbar is completely visible. When scrollValue is > rangeDifference, offset takes a negative value, so the toolbar is partially or totally off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollValue.

💬 That’s why we need to pass in the scroll value/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire ScrollState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

Scroll behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

EnterAlwaysState

🔷 Create a new class named EnterAlwaysState and make it a subclass of DynamicOffsetScrollFlagState:

🔷 Override its scrollOffset property as follows:

scrollOffset takes a value between 0 and maxHeight. Both, offset and height, depend on this member variable to calculate their values.

🔷 Override its scrollValue property as follows:

By calculating delta, we can deduce the scrolling direction and its length. A negative delta means that the list has been scrolled down. A positive delta means that the list has been scrolled up. The member variable scrollOffset is updated by subtracting delta from it, limiting its value between 0 and maxHeight. Finally, the value received by the custom setter as argument is assigned to the member variable _scrollValue, which works as a kind of backing property.

💬 0 is the initial value of scrollValue. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than 0.

🔷 Override its height property as follows:

💬 rangeDifference = maxHeight — minHeight.

height takes a value between minHeight and maxHeight. When scrollOffset is 0, height is equal to maxHeight, which means that the toolbar is expanded. When scrollOffset is ≥ rangeDifference, height is equal to minHeight, which means that the toolbar is collapsed.

🔷 Override its offset property as follows:

💬 rangeDifference = maxHeight — minHeight.

offset takes a value between 0 and -minHeight. When scrollOffset is ≤ rangeDifference, offset is 0, which means that the toolbar is completely visible. When scrollOffset is > rangeDifference, offset takes a negative value, so the toolbar is partially or totally off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save four properties: minHeight, maxHeight, scrollValue and scrollOffset.

💬 That’s why we need to pass in the scroll value/position and the scroll offset as arguments to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire EnterAlwaysState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

EnterAlways behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

EnterAlwaysCollapsedState

🔷 Create a new class named EnterAlwaysCollapsedState and make it a subclass of DynamicOffsetScrollFlagState:

🔷 Override its scrollOffset property as follows:

scrollOffset takes a value between 0 and minHeight. It works as a backing property for the offset property.

🔷 Override its scrollValue property as follows:

By calculating delta, we can deduce the scrolling direction and its length. A negative delta means that the list has been scrolled down. A positive delta means that the list has been scrolled up. The member variable scrollOffset is updated by subtracting delta from it, limiting its value between 0 and minHeight. Finally, the value received by the custom setter as argument is assigned to the member variable _scrollValue, which works as a kind of backing property.

💬 0 is the initial value of scrollValue. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than 0.

🔷 Override its height property as follows:

💬 rangeDifference = maxHeight — minHeight.

height takes a value between minHeight and maxHeight. When scrollValue is 0, height is equal to maxHeight, which means that the toolbar is expanded. When scrollValue is ≥ rangeDifference, height is equal to minHeight, which means that the toolbar is collapsed.

🔷 Override its offset property as follows:

This one is pretty straightforward. All it does is return the value of the member variable scrollOffset, which works as a backing property. When scrollOffset is equal to 0, offset is also 0, so the toolbar is completely visible. When scrollOffset is equal to minHeight, offset is -minHeight, so the toolbar is completely off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save four properties: minHeight, maxHeight, scrollValue and scrollOffset.

💬 That’s why we needed to pass in the scroll value/position and the scroll offset as arguments to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire EnterAlwaysCollapsedState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

EnterAlwaysCollapsed behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

ExitUntilCollapsedState

🔷 Create a new class named ExitUntilCollapsedState and make it a subclass of ScrollFlagState:

🔷 Override its scrollValue property as follows:

This one is pretty straightforward. All it does is update its member variable _scrollValue, which works as a kind of backing property.

💬 0 is the initial value of scrollValue. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than 0.

🔷 Override its height property as follows:

💬 rangeDifference = maxHeight — minHeight.

height takes a value between minHeight and maxHeight. When scrollValue is 0, height is equal to maxHeight, which means that the toolbar is expanded. When scrollValue is ≥ rangeDifference, height is equal to minHeight, which means that the toolbar is collapsed.

🔷 Override its offset property as follows:

Since the toolbar is completely visible and fixed, we have to set its offset property equal to 0.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollValue.

💬 That’s why we needed to pass in the scroll value/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire ExitUntilCollapsedState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

ExitUntilCollapsed behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

💬 If you found this article useful, 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.