Generic collapsing layout with Jetpack Compose

Ayushi Gupta
3 min readFeb 12, 2023
Let’s do this !!

Hello coders !!

There are many blogs out there to implement collapsing toolbar layouts in jetpack compose. But what about the situation where you want to stick the toolbar at top of the screen, but would surely like to stick the tab layout to the top?

In general, how to make a layout collapsable rather than just a toolbar :)

Let’s try to implement something like this:

Let’s dig deep into the main content, and what it looks like. So I used Scaffold-the basic material design visual layout structure.

Inside Scaffold, things look something like this :

@Composable
fun MainContent() {
Scaffold(
topBar = {
TopAppBar(
// Will fill in the details of toolbar
)
},
content = {
CollapsingLayout( // Will fill in the details of layout)
},
bottomBar = { }
)
}

Before going into details of Collapsing layout, let’s know a bit about NestedScrollConnection.

Nested Scroll Connection

It is an interface to connect to the nested scroll system. You can pass this connection to the nestedScroll modifier to participate in the nested scroll hierarchy and to receive nested scroll events when they are dispatched by the scrolling child (scrolling child - the element that actually receives scrolling events and dispatches them via NestedScrollDispatcher).

There are various callbacks which are can be received when you wire to this interface. But what is useful for today’s purpose is:

fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero

Pre-scroll event chain: It is called by the children to allow parents to consume a portion of a drag event beforehand. The received parameters are available — the delta available to consume for pre-scroll and source — the source of the scroll event and this function returns the amount of this connection consumed.

fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero

Post-scroll event pass: This pass occurs when the dispatching (scrolling) descendant made their consumption and notifies ancestors of what’s left for them to consume. The received parameters are available — the amount of delta available for this connection to consume, consumed — the amount that was consumed by all nested scroll nodes below the hierarchy and source — the source of the scroll event and this function returns the amount of this connection consumed.

Now we have an idea of what we can do with nested scroll connection, pre-scroll, and post-scroll event pass.

Let’s try to use it in our Collapsing Layout.

Collapsing layout consists of a Box layout and within that, we have two sections one for collapsing top and the rest for the body of the screen.

What makes this layout different is the offset calculation and wiring it through a nested scroll connection.

var offset by remember { mutableStateOf(0f) }
 fun calculateOffset(delta: Float): Offset {
val oldOffset = offset
val newOffset = (oldOffset + delta).coerceIn(-collapsingTopHeight, 0f)
offset = newOffset
return Offset(0f, newOffset - oldOffset)
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
when {
available.y >= 0 -> Offset.Zero
offset == -collapsingTopHeight -> Offset.Zero
else -> calculateOffset(available.y)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset =
when {
available.y <= 0 -> Offset.Zero
offset == 0f -> Offset.Zero
else -> calculateOffset(available.y)
}
}
}

Now, what’s remaining it’s just to use this calculated nested scroll connection in the box layout modifier for top content and body content and you are done.

More details on the above implementation could be found here: https://github.com/droidyayu/CollapsingLayout

Happy coding !!

--

--