Is there a common strategy for implementing a "sti...
# compose
s
Is there a common strategy for implementing a "sticky on scroll up" header in a LazyList? i.e. a header which hides itself while the user is scrolling down, but reappears immediately when the user scrolls up. I'm currently trying to jank it together with derived states of the LazyListState and it's not quite working right.
s
You can use nested scrolling. I'll paste the composable I use.
Copy code
@Composable
fun NestedScrollOffsetBox(
    toolbarHeight: Dp,
    modifier: Modifier = Modifier,
    content: @Composable NestedBoxScope.() -> Unit,
) {
// here we use LazyColumn that has build-in nested scroll, but we want to act like a
    // parent for this LazyColumn and participate in its nested scroll.
    // Let's make a collapsing toolbar for LazyColumn
    val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
    // our offset to collapse toolbar
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
    // now, let's create connection to the nested scroll system and listen to the scroll
    // happening inside child LazyColumn
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // try to consume before LazyColumn to collapse toolbar if needed, hence pre-scroll
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                // here's the catch: let's pretend we consumed 0 in any case, since we want
                // LazyColumn to scroll anyway for good UX
                // We're basically watching scroll without taking it
                return Offset.Zero
            }
        }
    }

    Box(modifier.clipToBounds().fillMaxSize().nestedScroll(nestedScrollConnection)) {
        with(NestedBoxScopeImpl(this, toolbarOffsetHeightPx.value)) {
            content()
        }
    }
}

interface NestedBoxScope : BoxScope {
    fun Density.getOffset(): IntOffset
}

class NestedBoxScopeImpl(
    private val boxScope: BoxScope,
    private val offsetPx: Float,
) : NestedBoxScope {
    override fun Modifier.align(alignment: Alignment): Modifier {
        return with(boxScope) { this@align.align(alignment) }
    }

    override fun Modifier.matchParentSize(): Modifier {
        return with(boxScope) { this@matchParentSize.matchParentSize() }
    }

    override fun Density.getOffset(): IntOffset {
        return IntOffset(x = 0, y = offsetPx.roundToInt())
    }
}
🙌 1
I'm not totally sure where I got that from. The usage is then something like:
Copy code
val height = 72.dp
NestedScrollOffsetBox(height) {
            List()
            Header(
                modifier = Modifier
                    .height(height)
            )
        }
s
Thank you for the insight!! I'm seeing similar implementations in material3's
ExitUntilCollapsedScrollBehavior
and in this article on nested scrolling. Adapting these seems like a good approach! https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]rScrollBehavior&ss=androidx%2Fplatform%2Fframeworks%2Fsupport https://medium.com/androiddevelopers/understanding-nested-scrolling-in-jetpack-compose-eb57c1ea0af0
138 Views