Skip to main content

Collapsible header in Jetpack Compose using NestedScrollConnection and SubComposeLayout

Shoaib MushtaqDecember 11, 2024About 7 minJavaKotlinAndroidArticle(s)blogdroidcon.comjavakotlinandroid

Collapsible header in Jetpack Compose using NestedScrollConnection and SubComposeLayout 관련

Android > Article(s)

Article(s)

Collapsible header in Jetpack Compose using NestedScrollConnection and SubComposeLayout
In Jetpack Compose, building a collapsible header with a custom navigation bar can be straightforward using NestedScrollConnection—provided the header has fixed expanded and collapsed heights. However, when the header height is dynamic and depends on its content (e.g., based on backend responses), things get tricky. Using onGloballyPositioned to measure the header's height alone may not suffice. To address this, I combined NestedScrollConnection with SubComposeLayout, as it handles dynamic header content effectively.

In Jetpack Compose, building a collapsible header with a custom navigation bar can be straightforward using NestedScrollConnection—provided the header has fixed expanded and collapsed heights. However, when the header height is dynamic and depends on its content (e.g., based on backend responses), things get tricky. Using onGloballyPositioned to measure the header’s height alone may not suffice. To address this, I combined NestedScrollConnection with SubComposeLayout, as it handles dynamic header content effectively.


Our Goal: The final header states

Let’s start by looking at the two final states of the header that we’ll achieve using NestedScrollConnection and SubComposeLayout in Jetpack Compose.

Header transition from expanded form to collapsed form and vice versa
Header transition from expanded form to collapsed form and vice versa

UI Composition Overview

To better understand how this UI is structured, let’s break it down. The layout uses a Box composable containing a Column. Within the Column, we have two key components: the ExpandedHeader and the LazyColumn. I’ll dive deeper into the nestedScroll(connection) and scrollable implementations in the following sections.


Breaking Down ExpandedHeader

The ExpandedHeader consists of two parts: the header and the navigation bar. To implement this, we use SubComposeLayout, creating two placeables—one for the header and another for the navigation bar. The HeaderContent represents the expanded state, while the NavBar corresponds to the collapsed state during transitions.
If you’re new to SubComposeLayout in Jetpack Compose, I highly recommend exploring these resources for a deeper understanding: SubComposeLayoutSample and Advanced Layouts in Compose.


The Role of NestedScrollConnection

By default, the header isn’t scrollable — only the lazy list is. However, our goal is to allow the header to move upward in sync with the scroll offset of the lazy list. This is where NestedScrollConnection becomes essential.

For a deeper dive into NestedScrollConnection, check out this blog post (androiddevelopers). Below, I’ll share my implementation of NestedScrollConnection, focusing on its onPreScroll and onPostScroll overrides.


Implementing NestedScrollConnection with the header?

Here’s how we integrate NestedScrollConnection within our Activity and composables to enable smooth interactions between the header and the lazy list.


Bring It All Together

When all the pieces of this puzzle come together, the result is seamless. As the lazy list scrolls, the header scrolls along with it. Once the header reaches a specific progress, we dynamically adjust the alpha value of the NavigationBar background, its icons, and the title for a smooth transition effect.


Challenges! Faced 🚧 and Solved💪

1.

Calculation of header offset and progress was a challenge and we have to do some Maths here to calculate headerOffset and progress which we will use to adjust the height of header and alpha of navBar when lazy list scrolls up

2.

When we scrolls the list up, the header scrolls up first and then the list. On the other hand, when I scroll down the list, the header was scrolling down first and then the list was scrolling but my requirement was that we we scroll down the list, we first scroll down the list untill it reaches to first item and then we scroll down the header. To solve this case, I added this below code snippet in onPreScroll and passing the zero offset when we scroll downward to pass it to the Node consumption phase. — More details on Node consumption phase is in this blogpost (androiddevelopers)

// ...
/**
 * when direction is negative, meaning scrolling downward,
 * we are not consuming delta but passing it for Node Consumption
 */
if (delta >= 0f) {
    return Offset.Zero
}
// ...

3.

If I tried to scroll the header by dragging the header part(not the lazy list), It was not scrolling because its was a Column with no scrollable behavior so to solve this case and make the header scrollable even if we drag the header part without touching the lazy list. Here is how I did it.

4.

Here is how we are adjusting height of header based on the header offset received through NestedScrollConnection , and placing the placeables calculated with SubComposeLayout .

5.

In this way, we are calculating alpha value based on the progress received from NestedScrollConnection and changing the alpha of Navigation Bar Composable

If you’d like to see the complete implementation in one place, feel free to check out this repository.

shoaibmushtaq25/CollapsibleHeader

For more details, please refer to these resources.

Jetpack Compose UI App Development Toolkit - Android Developers
Discover Jetpack Compose, Android's UI app development toolkit and resources that can help accelerate the creation of your app.
SubcomposeLayoutSample.kt - Android Code Search

Search and explore code
Material Components  |  Jetpack Compose  |  Android Developers
Material components allow you to build detailed interfaces in line with Material Design principles.

Feel free to ask any questions you may have — I’d be happy to collaborate and discuss further.
I hope you found this helpful, and thank you for reading!

Info

This article is previously published on proandroiddev)

Collapsible header in Jetpack Compose using NestedScrollConnection and SubComposeLayout
In Jetpack Compose, building a collapsible header with a custom navigation bar can be straightforward using NestedScrollConnection—provided…
Collapsible header in Jetpack Compose using NestedScrollConnection and SubComposeLayout

In Jetpack Compose, building a collapsible header with a custom navigation bar can be straightforward using NestedScrollConnection—provided the header has fixed expanded and collapsed heights. However, when the header height is dynamic and depends on its content (e.g., based on backend responses), things get tricky. Using onGloballyPositioned to measure the header's height alone may not suffice. To address this, I combined NestedScrollConnection with SubComposeLayout, as it handles dynamic header content effectively.