Collapsing Toolbar made easy with Compose

Before Motion Layout, Collapsing Toolbar has always been an interesting subject on Android. Collapsing effects usually brought complexity to our code if were were required to design custom collapsing animations.

Let’s create our own Collapsing Toolbar!

Our screen contains a header representing the food category and some items in a list below that make up some dishes for that particular food:

As there are strong signals indicating that MotionLayout could be released for Compose, if you need complex collapsing effects you might want to hold off until then.

But what if you want a simple approach to implementing a collapsing toolbar in Compose?

Let’s dive in

We saw the following collapsing effect: as the user scrolls down the list of dishes, our header shrinks. More precisely, the profile picture of the food category becomes smaller while the description text has less lines.

Let’s get to know our starting point. We have a fixed header for now representing the food category and the food items in a list below:

Let’s get to it!

Let’s check out our existing composables for this screen. We should have a parent Column that holds our initial fixed header (we have yet to make it collapsible) and the list of dishes:

For this, we have the first composable CategoryDetailsCollapsingToolbar and our list of dishes populated through a LazyColumn that takes a list of FoodItemRow composables. The actual food category and its corresponding dishes are obtained from the state object of the root composable which is also the screen: FoodCategoriesDetailsScreen.

Since our toolbar is initially fixed in size, let’s check out the initial code for the CategoryDetailsCollapsingToolbar as well:

The toolbar is pretty simple as it contains an Image with a fixed size and a Column that holds the two Text Composables for the food category title and description. We can also see that the description Text composable has a fixed number of lines.

How can we transform our toolbar into a collapsing one?

We know that while the user scrolls down our LazyColumn holding the dishes list, we want the profile picture of the food category to become smaller and the description text to have less lines.

It’s clear that inside the CategoryDetailsCollapsingToolbar we should no longer have fixed values for the Image (which is now 128.dp) or fixed values for the maximum number of lines of the description text (which now is 6).

Instead, we want to make the Image size to vary from 64.dp (collapsed) to 128.dp (expanded), while the maximum number of lines of the description text should vary from 3 (collapsed) to 6 (expanded).

Defining an offset

An offset in our case should be a value that decreases whether the user is scrolling down and increases as the user scrolls up. This way, we can decrease the size of the picture and the number of lines for the description as the scroll scrolls down the list, and then increase them back when he scrolls up.

In a regular scenario one could bind a NestedScrollConnection to the scaffold and calculate the offset based on that. But such implementation increases the complexity of our code and we’re here to discuss an easy implementation!

Calculating an offset for LazyColumn

A regular Column composable could gain access to the scroll state through the rememberScrollState API and then easily define an offset depending whether the user is scrolling up or down.

A LazyColumn on ther hand has access to LazyListState that doesn’t provide the scroll offset out of the box. But we can calculate it later! First, let’s add it:

By registering a LazyListState object to our LazyColumn, we gain access to two important fields:

  • firstVisibleItemScrollOffset – offset resets every time a new row within the list is visible as the first item. If you scroll past the first row, you will see the offset increase from 0, but as soon as you get to the second row, that row becomes the new first visible item. Therefore, the offset resets to 0 again and it starts increasing as you scroll. This repeats as you scroll past the items within your list.
  • firstVisibleItemIndex – this field tells us the index of the first visible item within the list. If we were to scroll past two items within our list, this field would be 2 because the third element would now be the first visible item. This changes as you scroll past the items within your list.

It’s not exactly what we wanted, but if we consider that scrolling past the first item is enough to collapse/expand the toolbar, we can do calculate the offset by doing some simple calculations:

We first divide the firstVisibleItemScrollOffset by 600 in order to get a smaller value of the first item’s scroll offset. The result increases/decreases as we scroll past every item.

In order to obtain a linear increase of the scroll offset (and not have it reset) we also add the firstVisibleItemIndex which is incremented after every item scroll. Finally, we take the minimum value between 1 and the calculated offset therefore getting our final scrollOffset.

If the user scrolls past the first item, the firstVisibleItemIndex will become 1 therefore we will automatically always have the scrollOffset as 1 if the users tries to scroll lower than the first item. This allows us to only shrink the toolbar on the scroll of the first item and not forever. We wouldn’t want to shrink the toolbar progresively until it becomes too small.

Note: Since our scrollOffset variable depends on a compose remember variable called scrollState, whenever the user scrolls, the whole widget tree recomposes, recalculating the offset and allowing us to change the size of the composables dynamically.

Enough with the maths!

Let’s pass the offset to our CategoryDetailsCollapsingToolbar composable so it can use it to collapse/expand its content:

Next up, let’s calculate the size of the image and the number of lines for the description dynamically based on the newly received scrollOffset.

We know that the image’s size should vary from 64.dp to 128.dp, so we take the maximum between 64.dp and 128.dp*scrollOffset and animate the change through the animateDpAsState state object.

Since scrollOffset starts from 1 and decreases as the user scrolls, our image will initially be of size 128.dp and then it will decrease as the user scrolls to a minimum of 64.dp.

Alternatively, the number of lines of our description Text Composable should vary from 3 to 6, so we take the maximum between 3 and the 6*scrollOffset. Finally, we pass the dynamic values to the composables:

And that’s it!

We got our desired result. And there was little to no complexity added to our code as we riddled it with a simple math formula.

Let’s check out the final result:

Since the above example is ultra-simplified in terms of the actual modifiers of the Composables, I had to focus on the core of this article: the collapsing effect.

Check out the complete code for yourself at this repository or navigate directly to this file that contains the complete class.


I want you focused so take a break, and see ya in the next article!

8+
%d bloggers like this: