android-jetpack-composenested-scroll

Is there a way to remove space in App Bar NestedScrollConnection?


I am trying to collapsing the App Bar with my Custom NestedScrollConnection but it make space in App Bar place when I Scroll. The App Bar goes up when I scroll but the space of App Bar in the Background of App Bar doesn't go up?

My Custom NestedScrollConnection Class:

class CollapsingAppBarNestedScrollConnection(
    val appBarMaxHeight: Int,
) : NestedScrollConnection {

    var appBarOffset by mutableIntStateOf(0)
        private set

    override fun onPreScroll(
        available: Offset,
        source: NestedScrollSource,
    ): Offset {
        val delta = available.y.roundToInt()
        val newsOffset = appBarOffset + delta
        val previousOffset = appBarOffset
        appBarOffset = newsOffset.coerceIn(
            -appBarMaxHeight,
            0
        )
        val consumed = appBarOffset - previousOffset

        return Offset(0f, consumed.toFloat())
    }
}

MY UI:

val density = LocalDensity.current

val appBarMaxHeight = with(density) { (70).dp.roundToPx() }

val connection = remember(appBarMaxHeight) {
    CollapsingAppBarNestedScrollConnection(appBarMaxHeightPx)
}

Scaffold(
   topBar = {
      TopAppBar(
         title = {
            Text("Top App Bar")
         },
         modifier = Modifier
             .offset { IntOffset(0, connection.appBarOffset) }
      )
   },
   modifier = Modifier
       .fillMaxSize()
       .nestedScroll(connection)
) {
   // Content
}

Video gif, before

Video gif, After


Solution

  • When you use the offset Modifier, the space where the Composable originally was remains reserved. This is because

    it avoids recomposition when the offset is changing, and also adds a graphics layer that prevents unnecessary redrawing of the context when the offset is changing.

    Thus, the offset is applied on another layer and does not actually impact the Composable hierarchy. This means that you can't use the space where the TopAppBar originally was for other Composables.

    To achieve this, you would usually use a default NestedScrollConnection like TopAppBarDefaults.enterAlwaysScrollBehavior.

    If you actually need a custom NestedScrollConnection the TopAppBar to shift out of the screen at scrolling, implement this using the height Modifier together with wrapContent:

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun MyCollapsibleTopAppBarDemo() {
    
        val density = LocalDensity.current
        val myNestedScrollConnection = remember {
            MyNestedScrollConnection(TopAppBarDefaults.TopAppBarExpandedHeight.dpToPx(density))
        }
    
        Scaffold(
            modifier = Modifier
                .fillMaxSize()
                .nestedScroll(myNestedScrollConnection),
            topBar = {
                CenterAlignedTopAppBar(
                    modifier = Modifier
                        .height(myNestedScrollConnection.currentHeaderHeightPx.toInt().pxToDp(density))
                        .wrapContentHeight(Alignment.Bottom, true),
                    colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
                        containerColor = Color.LightGray
                    ),
                    title = {
                        Text("Demonstration")
                    }
                )
            }
        ) { innerPadding ->
            LazyColumn(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding)
            ) {
                items(50) { item ->
                    Text("Item $item", modifier = Modifier.padding(16.dp))
                }
            }
        }
    }
    
    fun Dp.dpToPx(density: Density) = with(density) { this@dpToPx.toPx() }
    fun Int.pxToDp(density: Density) = with(density) { this@pxToDp.toDp() }
    

    Update your custom NestedScrollConnection like this:

    class MyNestedScrollConnection(
        val appBarMaxHeight: Float,
    ) : NestedScrollConnection {
    
        var currentHeaderHeightPx by mutableFloatStateOf(appBarMaxHeight)
            private set
    
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
            val delta = available.y
            // add this if statement when you only want the TopAppBar to appear when scrolled fully to the top
            if (delta >= 0) {
                return Offset.Zero
            }
            // 
            val newHeaderHeightPx = currentHeaderHeightPx + delta
            val previousHeaderHeightPx = currentHeaderHeightPx
            currentHeaderHeightPx = newHeaderHeightPx.coerceIn(0f, appBarMaxHeight)
            val consumed = currentHeaderHeightPx - previousHeaderHeightPx
            return Offset(x = 0f, y = consumed)
        }
    
        // additionally add this fun when you only want the TopAppBar to appear when scrolled fully to the top
        // this code is very similar to onPreScroll, so you could extract a common method for it
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            val delta = available.y
            val newHeaderHeightPx = currentHeaderHeightPx + delta
            val previousHeaderHeightPx = currentHeaderHeightPx
            currentHeaderHeightPx = newHeaderHeightPx.coerceIn(0f, appBarMaxHeight)
            val consumedLocally = currentHeaderHeightPx - previousHeaderHeightPx
            return Offset(x = 0f, y = consumedLocally)
        }
    }
    

    Output:

    Screen Recording

    Note:

    This does not animate the CenterAlignedTopAppBar color when the content is scrolled.