Collapsing Toolbar in Jetpack Compose | ‘LazyColumn’ version — Part 1
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 LazyCatalog
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 theLazyCatalog
.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
andLazyCatalog
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 LazyCatalog
composables.
🔷 Open the file Catalog.kt
.
🔷 Copy the body of Catalog
below and replace the TODO()
statement with it.
We also need a NestedScrollConnection
object which contains callbacks that will be called when the list dispatches scrolling events, so we can use these callbacks to make the toolbar participate in the nested scroll chain.
🔷 Add the following code before the Box
block:
By implementing the onPreScroll
callback, we let the toolbar be the first consuming/reacting to the scrolling event. We will finish the implementation of onPreScroll
later, but for now all we need is assign the NestedScrollConnection
object to the modifier of the parent composable, which in this case is the Box
component.
🔷 Assign the nestedScrollConnection
object to the modifier of the Box
as follows:
🔗 If you need more details about how the
NestedScrollConnection
object works, take a look at its official documentation here.
🔶 This is what the code inside Catalog.kt
should look like:
▶️ 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 41— and its modifier property height
—line 46 — . Neither should have a fixed value. For its offset, we are going to call its modifier function graphicsLayer
, setting its translationY
value.
Similarly, the LazyCatalog
composable is receiving two fixed values: its parameter contentPadding
—line 37 — and its offset through its modifier function graphicsLayer
— line 35 — . These values shouldn’t be fixed since both depend not only on the toolbar state, but also on its type.
Since progress
, height
and offset
are interrelated properties that depend on the LazyListState
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.
Besides those three properties, there is one more thing that we must take into account in order to get a smooth scrolling effect. We need to consume part of the scrolling value which is received in the available
parameter of onPreScroll
. There are some operations that we will have to make in order to calculate how much the toolbar is going to consume, since onPreScroll
is required to return that value. That way, the rest of the components involved in the nested scroll chain will behave just like we expect them to.
— 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
:
🔷 Replace the onPreScroll
callback implementation with the following code:
🔷 Replace all the fixed parameters of CollpasingToolbar
and LazyCatalog
that we set before, with the ones provided by whichever scroll flag implementation we’re using:
Let’s leave a fixed value for the bottom padding — line 15 — for now. We’ll work on that later.
🔶 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 a parameter.
🔷 Add the parameter heightRange: IntRange
to its constructor:
🔷 Validate heightRange
with a require
statement inside an init
block.
🔷 Add the following member variables for internal control:
🔷 Override its height
property making use of its custom getter with the following calculation:
💬
rangeDifference = maxHeight — minHeight
.
height
takes a value between minHeight
and maxHeight
. When scrollOffset
is ≥ rangeDifference
, the toolbar is collapsed. When scrollOffset
is 0
, the toolbar is expanded.
🔷 Override its progress
property making use of its custom getter with the following calculation:
💬
rangeDifference = maxHeight — minHeight
.
progress
takes a value between 0
and 1
, which correspond to the toolbar collapsed and expanded states respectively.
🔷 Override its consumed
property making use of its custom getter returning the value of _consumed
which works as a backing property:
consumed
corresponds to the length that the toolbar takes from the total scrolling length provided by the NestedScrollConnection
object.
🔷 Override its scrollTopLimitReached
property setting its value to true
:
scrollTopLimitReached
works as a flag that lets us know if the first item of the list is completely visible.
🔶 And that’s all for this class. Its complete code should look like this:
— Step 4 : A little more abstraction
The bottom padding that we have to add to the list depends on whether the toolbar has a fixed position or not. If it does, then a bottom padding with a size equal to its minimum height is required. Otherwise, the last elements of the list would remain completely, or at least partially, off-screen. On the other hand, if the toolbar position is not fixed, then we don’t need to add any padding to the list.
We need a way to find out if the toolbar position is fixed or not. We can do that in two different ways:
- Adding an immutable boolean property to the interface.
- Applying a little more abstraction, so we can tell if a concrete implementation is of certain type or not.
I like the latter approach better, so we need to create a new abstract class to classify any fixed toolbar that we decide to implement.
🔷 Create a new abstract class named FixedScrollFlagState
inside the toolbar
package and make it a subclass of ScrollFlagState
:
All we need to do is set its property offset
equal to 0f
and make it final
, so it can’t be overridden by any subclass.
Now we can replace the fixed bottom padding that we were passing in to LazyCatalog
with one that depends on the ToolbarState
implementation type.
🔷 Open the file Catalog.kt
and replace the line that corresponds to the contentPadding
parameter of LazyCatalog
with the following:
🔶 This is what the code inside Catalog.kt
should look like:
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 _scrollOffset
property as follows:
_scrollOffset
works as a backing property for scrollOffset
.
🔷 Override its scrollOffset
property as follows:
If the top of the list has been reached, _scrollOffset
will be updated with a value between 0
and maxHeight
. Otherwise, _scrollOffset
will remain the same. Also, _consumed
will be updated accordingly.
🔷 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 three properties: minHeight
, maxHeight
and scrollOffset
.
💬 That’s why we need to pass in the scroll offset/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.
We’re not done yet
Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:
🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.
Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…
…or just take a look at the following image:
To make the toolbar enter the screen and expand after a fling gesture, we need to implement the onPostFling
callback inside the NestedScrollConnection
object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.
💬 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 ScrollFlagState
:
🔷 Override its _scrollOffset
property as follows:
_scrollOffset
will work as a backing property for scrollOffset
.
🔷 Override its scrollOffset
property as follows:
_scrollOffset
will be updated with a value between 0
and maxHeight
. _consumed
will also be updated accordingly.
🔷 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 three properties: minHeight
, maxHeight
and scrollOffset
.
💬 That’s why we need to pass in the scroll offset/position as argument 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 ScrollFlagState
:
🔷 Override its _scrollOffset
property as follows:
_scrollOffset
will work as a backing property for scrollOffset
.
🔷 Override its scrollOffset
property as follows:
💬
rangeDifference = maxHeight — minHeight
.
If the top of the list has been reached, _scrollOffset
will be updated with a value between 0
and maxHeight
. Otherwise, _scrollOffset
will be updated with a value between rangeDifference
and maxHeight
. Also, _consumed
will be updated accordingly.
🔷 Override its offset
property as follows:
💬
rangeDifference = maxHeight — minHeight
.
offset
takes a value between 0
and -minHeight
. When scrollOffset
is ≤ rangeDifference
, the toolbar is completely visible. When scrollOffset
is > rangeDifference
, 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 scrollOffset
.
💬 That’s why we need to pass in the scroll offset/position as argument 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.
We’re not done yet
Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:
🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.
Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…
…or just take a look at the following image:
To make the toolbar enter the screen and expand after a fling gesture, we need to implement the onPostFling
callback inside the NestedScrollConnection
object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.
💬 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 FixedSrollFlagState
:
🔷 Override its _scrollOffset
property as follows:
_scrollOffset
will work as a backing property for scrollOffset
.
🔷 Override its scrollOffset
property as follows:
💬
rangeDifference = maxHeight — minHeight
.
If the top of the list has been reached, _scrollOffset
will be updated with a value between 0
and rangeDifference
. Otherwise, _scrollOffset
will remain the same. Also, _consumed
will be updated accordingly.
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 scrollOffset
.
💬 That’s why we need to pass in the scroll offset/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.
We’re not done yet
Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:
🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.
Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…
…or just take a look at the following image:
To make the toolbar expand after a fling gesture, we need to implement the onPostFling
callback inside the NestedScrollConnection
object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.
💬 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.
- 📖 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.