androidkotlinandroid-jetpack-composeandroid-nestedscrollview

Nested Scrolling with collapsible header not working


I have been trying to collapse/expand a header when lazy column list scroll up or down using nested scrolling. I have been using following code

@Composable
fun ScrollableScreenWithCollapsibleHeader() {
val headerHeight = 150.dp // Initial header height
var headerOffset by remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
            val delta = available.y
            val newOffset = headerOffset + delta
            headerOffset = newOffset.coerceIn(0f, headerHeight.value)
            return Offset(x = 0f, y = newOffset - headerOffset)
        }
    }
}

Box(modifier = Modifier.fillMaxSize()) {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
            .padding(top = headerHeight) // Add padding for the header
    ) {
        items(items) { item ->
            Text(item, modifier = Modifier.padding(16.dp))
        }
    }

    MyHeader(
        modifier = Modifier
            .fillMaxWidth()
            .height(headerHeight)
            .offset { IntOffset(x = 0, y = headerOffset.toInt()) }
    )
}


@Composable
fun MyHeader(modifier: Modifier = Modifier) {
// ... Your header content here ...
Box(modifier = modifier.background(Color.LightGray)) {
    Text("Header", modifier = Modifier.padding(16.dp))
}
}

But list is not even scrolling, other than collapsing or expanding header. What i am doing wrong


Solution

  • There are a few issues in your code:

    I would suggest the following refactored code:

    @Composable
    fun ScrollableScreenWithCollapsibleHeader() {
    
        val density = LocalDensity.current
    
        var minHeaderHeightPx by remember { mutableFloatStateOf(-1f) }
        var maxHeaderHeightPx by remember { mutableFloatStateOf(-1f) }
        var currentHeaderHeightPx by remember { mutableFloatStateOf(0f) }
    
        val nestedScrollConnection = remember {
            object : NestedScrollConnection {
                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                    val delta = available.y
                    val newHeaderHeightPx = currentHeaderHeightPx + delta
                    currentHeaderHeightPx = newHeaderHeightPx.coerceIn(minHeaderHeightPx, maxHeaderHeightPx)
                    val unconsumedPx = newHeaderHeightPx - currentHeaderHeightPx
                    return Offset(x = 0f, y = delta - unconsumedPx)
                }
            }
        }
    
        Column(
            modifier = Modifier
                .fillMaxSize()
                .nestedScroll(nestedScrollConnection)
        ) {
            MyHeader(
                modifier = if (maxHeaderHeightPx == -1f) {
                    Modifier.onGloballyPositioned { coordinates ->
                        currentHeaderHeightPx = coordinates.size.height.toFloat()
                        maxHeaderHeightPx = coordinates.size.height.toFloat()
                        minHeaderHeightPx = maxHeaderHeightPx / 2  // set min height to 50% of full height
                    }
                } else {
                    Modifier
                        .height(currentHeaderHeightPx.toInt().pxToDp(density))
                        .clipToBounds()
                }
            )
            LazyColumn(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
            ) {
                items(50) { item ->
                    Text("Item $item", modifier = Modifier.padding(16.dp))
                }
            }
        }
    }
    
    
    @Composable
    fun MyHeader(modifier: Modifier = Modifier) {
        Column(
            modifier = modifier
                .background(Color.LightGray)
                .fillMaxWidth()
                .wrapContentHeight(unbounded = true, align = Alignment.Bottom),
        ) {
            Text("Header A", fontSize = 35.sp)
            Spacer(Modifier.height(8.dp))
            Text("Header B", fontSize = 35.sp)
        }
    }
    
    fun Int.pxToDp(density: Density) = with(density) { this@pxToDp.toDp() }
    

    This code does the following things:

    Output:

    Screen Recording