Collapsing Toolbar in Jetpack Compose | ‘Column’ version
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:
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 theEagerColumn
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 theCollapsingToolbar
andEagerCatalog
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:
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 ofscrollValue
. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than0
.
🔷 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.
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 ofscrollValue
. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than0
.
🔷 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.
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 ofscrollValue
. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than0
.
🔷 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.
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 ofscrollValue
. It gets increased as the list is scrolled down and decreased as the list is scrolled up. It will never be lower than0
.
🔷 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.
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.
- 📖 You can find the API Guidelines for Jetpack Compose at https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md.
- 📖 You can find the official documentation of Restoring state in Compose at https://developer.android.com/jetpack/compose/state#restore-ui-state.
- 📖 You can find the official documentation about Getters and Setters, which includes custom accessors, backing fields and backing properties; at https://kotlinlang.org/docs/properties.html#getters-and-setters.