Does Material3 offer a TopAppBar which collapses c...
# compose
s
Does Material3 offer a TopAppBar which collapses completely (including the window insets) when scrolled up? Currently playing with a TopAppBarScrollBehavior set to
TopAppBarDefaults.enterAlwaysScrollBehavior()
and it seems like it collapses the content, but always keeps the window insets part at the top.
I hope this video illustrates what I mean here
And yes I am going edge to edge otherwise, if I remove the top app bar completely I get this, so that is not the issue here
Whipped this bad boy together
Copy code
@Composable
internal fun chatTopAppBarWindowInsets(
  windowInsets: WindowInsets,
  topAppBarScrollBehavior: TopAppBarScrollBehavior,
): WindowInsets {
  val density = LocalDensity.current
  val layoutDirection = LocalLayoutDirection.current
  var resultingInsets by remember { mutableStateOf(windowInsets) }
  LaunchedEffect(topAppBarScrollBehavior.state, density, layoutDirection) {
    snapshotFlow { topAppBarScrollBehavior.state.collapsedFraction }.collectLatest { fraction ->
      resultingInsets = windowInsets.times(fraction, density, layoutDirection)
    }
  }
  return resultingInsets
}

private fun WindowInsets.times(times: Float, density: Density, layoutDirection: LayoutDirection): WindowInsets {
  val multiplyBy = 1 - times
  return WindowInsets(
    left = (getLeft(density, layoutDirection) * multiplyBy).roundToInt(),
    right = (getRight(density, layoutDirection) * multiplyBy).roundToInt(),
    top = (getTop(density) * multiplyBy).roundToInt(),
    bottom = (getBottom(density) * multiplyBy).roundToInt(),
  )
}
Using
topAppBarScrollBehavior.state.collapsedFraction
basically and it does seem to work, while being like super quick about it, but that’s fine 😄 On the call-site I do this
Copy code
TopAppBarWithBack(
        scrollBehavior = topAppBarScrollBehavior,
        windowInsets = chatTopAppBarWindowInsets(TopAppBarDefaults.windowInsets, topAppBarScrollBehavior),
...
Will just use this for now until(if) something better pops up 😄
collapse_good.mp4
z
Which version of compose are you using? The content scrolls underneath the status bar for me, on the alpha (now beta) releases!
s
Is it using enterAlwaysScrollBehavior? I am just in the latest stable bom, so it definitely is very likely that they fixed it in the alphas. I can't really test the alphas because they made some breaking changes in some (unstable of course) things we use and I'm just waiting for the real release to come through and put the effort to migrate once instead of doing it potentially many times if the alphas break more apis 😅
z
Yeah, it works with any behaviour that hides it! I don't think I'm doing anything fancy to make that work, just does out of the box. Perhaps someone can verify, I can't look at my code right now 🫡
s
Yup then it should just work on the latest alphas. Thanks for the heads up! Will make sure to remove this workaround on the next version bump I do for m3 🤗
💪🏽 1
a
Well this is something I've been confused about, but I admit I haven't fiddled with it too much yet. In M3 you can set TopAppBar
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
which lets content scroll behind the status bar, because nothing's consuming it. But that screws up AppBar, because it will of course draw under the status bar now. I'd like a way to leave space for the status bar, then draw the AppBar, but also let AppBar collapse into the status bar. @Zoltan Demant perhaps you are doing something special. It doesn't work for:
Copy code
Column {
  TopAppBar(windowInsets = horizontal)
  Content()
}
Feels like we have to operate on collapsedFraction ourselves on top of what TopAppBar already does
s
Oh right, but I do have this code over there too to handle the consumed insets or not
Copy code
Column {
      val density = LocalDensity.current
      var topAppBarHeight by remember { mutableStateOf(0.dp) }
      TopAppBar(
        onNavigateUp = onNavigateUp,
        topAppBarScrollBehavior = topAppBarScrollBehavior,
        onSizeChanged = { with(density) { topAppBarHeight = it.height.toDp() } },
      )
      Box(
        modifier = Modifier
          .consumeWindowInsets(PaddingValues(top = topAppBarHeight)),
So now in your content, just use the insets as you wish, because if the top app bar is consumed then you know that it’s not gonna double apply it, if it’s not present then you will have them available to you
a
I think this is what Scaffold does already (is it not in M2?) but we intentionally moved away from it because it causes content to recompose, because the passed in PaddingValues changes on scroll. Noticeable jank with NavHost.
s
I do not use Scaffold anyway here. But yeah m3 scaffold handles this, m2 too if you use the accompanist one, unless they merged than into the real m2 dependency too.
z
I wonder if the scaffold is what makes the magic work though. Iirc, it consumes some window insets and applies them to the content, which sounds just about right for this case?
s
Yeah, I think that it marks the correct amount of window insets as consumed for the content, if it’s already consumed by the TopAppBar for example. Basically what I am doing in the snippet above. But it can do that in the layout itself since it’s using Subcomposition to get the size of all the elements there. Mine is probably lagging a bit behind since I am changing the state
onSizeChanged
and then it applies the right consumed insets number. Might be 1 frame late sometimes, but so would Subcomposition if I understand that correctly 😄
a
For posterity, fleshing out what I meant above.
Copy code
Scaffold(
  topBar = { TopAppBar(scrollBehavior = enterAlways) },
) { innerPadding ->
  // ^ on topBar expand/collapse, innerPadding changes, causes Content to recompose
  // Bad!
  Content(innerPadding)
}
An identical layout without the unnecessary recomposition would be:
Copy code
Column {
  TopAppBar(scrollBehavior = enterAlways) // <- expand/collapse still works
  Content() // <-- does not cause Content to recompose the entire time topBar does its thing
  // Good!
}
In other words, TopAppBar changes are contained to itself, as it should be. Why should sibling composables care about each other? It was a night and day difference for us when we tested between them, some alphas earlier. Not sure if anything's changed with Scaffold itself, or with Compose (we didn't have
experimentalStrongSkipping
for example), but I guess we'll revisit it later. We'd really like proper edge-to-edge as it looks nice.
👍 1
👀 1
z
Interesting, thanks for sharing @ascii 🫡
a
A little bit off-topic but as a user I prefer the content not going underneath status bar as it makes status bar content harder to recognize and I can't clearly see the app content anyway.
s
Yeah I see your point Albert. At the same time I find that having the status bar have a solid color and having the content get cut-off completely looks super ugly to me for some reason. And if you want a clear view of what the content shows, it’s always a matter of scrolling down a bit, which you would do in either scenario anyway. Now that does mean that it’s a net negative for the legibility of the status bar content itself, but I’ve become accustomed to just grabbing the status bar and dragging it down if there’s anything up there which looks important enough for me to read.
a
Same, it's more immersive. The status bar for me doesn't really have info that I need to see at all times. Still, it's subjective and the best thing would be to just allow the user to toggle what they'd like.