If I got a structure like this: ```Box { SomeScr...
# compose
s
If I got a structure like this:
Copy code
Box {
  SomeScrollableContent(Modifier.fillMaxSize())
  BottomAnchoredItem(Modifier.align(Alignment.BottomCenter))
}
Is there a way to make sure that my scrollable content is providing enough extra space at the end of the scrollable content so that when I’m scrolled all the way to the bottom the items are over this bottom anchored item?
Right now I’m doing something like
Copy code
@Composable
fun MyBox() {
  Box {
    val density = LocalDensity.current
    var bottomAnchoredItemHeight by remember { mutableStateOf(0.dp) }
    SomeScrollableContent(bottomAnchoredItemHeight, Modifier.fillMaxSize())
    BottomAnchoredItem(
      Modifier
        .align(Alignment.BottomCenter)
        .onPlaced { layoutCoordinates ->
          bottomAnchoredItemHeight = with(density) { layoutCoordinates.size.height.toDp() }
        },
    )
  }
}

@Composable
fun SomeScrollableContent(bottomAnchoredItemHeight: Dp, modifier: Modifier = Modifier) {
  Column(Modifier.verticalScroll(rememberScrollState()).padding(bottom = bottomAnchoredItemHeight)) {}
}
But since I’m relying on
onPlaced
to change some state this happens 1 frame late, and in general makes me feel like I’m missing a better approach.
f
why not make this a
Column
with the scrollable content having a weight?
s
The bottom anchored item is floating there, always at the bottom of the screen, while the scollable content is kinda “behind” that anchored item at the bottom. Think like a FAB.
f
ok, but I don't see why that precludes using a
Column
, the anchored content will still be at the bottom, the scrollable content will use all the remaining space after the anchored item is measured, which seems to be what you want
unless your anchored item is partially transparent and you want to scroll behind it, is that what you want?
s
I do want the scrollable content to show under that item too yes exactly. That item in particular is a button which has paddings around it where you can see behind it. I also need that content to show under the navigation bar since I’m going edge-to-edge on this screen.
f
ok, so an option may be to use a custom
Layout
then
s
Yeah, I was thinking about that, but since I’m having scrollable content and stuff I’d rather not go down that hole unless I really need to. And the solution here works but it’s 1 frame late. Maybe I can’t get an easy win here and improve this without something more complex like a custom
Layout
🤷‍♂️
f
SubcomposeLayout
might be the answer here as you can use a child's size as input for another child
but I've heard it comes with a performance penalty, so you might be better off with your 1 frame delay implementaion
s
Yeah, might revisit this in the future but it sounds like I’ll leave it like this for now unless someone comes up with “here’s this thing you can do and it just works” 😅 Thanks a lot for your help, I appreciate it!
a
You could use
Scaffold
here, which uses
SubcomposeLayout
under the hood to be able to provide the size of the bottom bar to the main content. I’d prefer that over the 1 frame delay (due to a cyclic composition dependency).
SubcomposeLayout
is more costly in general, so you’d want to avoid using it everywhere, but this is one of those cases where you can use it to have the size of one part of your layout inform what the composition should be in a different part.
s
I guess that’s true, missing 1 frame is worse than using a
SubcomposeLayout
. Does scaffold provide a slot for my use case?
bottomBar
maybe? 🤔 Plus I guess I’d have to use the one provided by accompanist insets-ui since I’m going edge to edge in this screen.
a
Scaffold
is probably going to do a bit more than you need, but you can try it out and then you may want to roll your own
SubcomposeLayout
. This should be about all you need:
Copy code
Scaffold(
    bottomBar = {
        BottomAnchoredItem(Modifier.align(Alignment.BottomCenter))
    }
) { innerPadding -> // inner padding will now have the height of the BottomAnchoredItem
    SomeScrollableContent(Modifier.fillMaxSize())
}
You’d only need the accompanist/insets-ui one to do to the analogous thing for the
topBar
with the Material 2
Scaffold
. (the default Material 2
Scaffold
doesn’t include the
topBar
height in the
innerPadding
), but the one in accompanist/insets-ui does)
s
Right this works! I got my
BottomAnchoredItem
like this (No need for the
Alignment.BottomCenter
anymore, since we’re not in a
BoxScope
so we can’t even access it if we wanted to):
Copy code
BottomAnchoredItem(
  modifier = Modifier
    .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal))
    .padding(16.dp),
) {
  Text(text = data.callToAction)
}
Then the
paddingValues
I get do in fact contain a ~100dp bottom padding which I can then use like
.verticalScroll(rememberScrollState()).padding(paddingValues)
on my column and it’s working perfectly fine!
One interesting thing here regarding insets. Before I had
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))
on my ScrollableContent too, in order to avoid the bottom bar, but since I’m doing that in my
BottomAnchoredItem
too, now that spacing is part of
paddingValues
provided by the Scaffold, it double applies that value 🙈 But then I remember you were doing something like this in the

CodeWithTheItalians

episode, so I tried adding
consumedWindowInsets
as a modifier to my scaffold.
Copy code
Scaffold(
  bottomBar = {
    BottomAnchoredItem(
      modifier = Modifier
        .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal))
        .padding(16.dp),
    )
  },
  modifier = Modifier.consumedWindowInsets(
    WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),
  )
) { paddingValues ->
This did not work as that now also consumed the paddings that the
BottomAnchoredItem
wanted to use. Moved that modifier down to the one child of the
Scaffold
content
Copy code
val bottomAnchoredButtonInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
Scaffold(
  bottomBar = {
    BottomAnchoredItem(
      modifier = Modifier
        .windowInsetsPadding(bottomAnchoredButtonInsets)
        .padding(16.dp),
    )
  },
  modifier = Modifier.consumedWindowInsets(bottomAnchoredButtonInsets)
) { paddingValues ->
  Box(Modifier.consumedWindowInsets(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal))) {}
}
And what do you know, this does work 🤯🤯🤯🤯 without having to go in that other composable and remove the insets in case idk, this changes sometime, or reusing that other component somewhere else etc. I guess this was not an exaggeration after all, that CWTI episode taught me so much about the inset APIs, I never would’ve even thought of doing smth like this before. Thanks a lot for that and the help here Alex! 🙌
Ouch, but then when on landscape mode, since I was consuming the horizontal insets too, the content was drawing behind the navigation bar. Since the
bottomBar
content on the Scaffold is not rotating with the phone obviously, I still need the horizontal paddings to propagate down to the child. Fixed it now by making the insets passed to
consumedWindowInsets
only take the
WindowInsetsSides.Bottom
so only considering those as consumed. Starting to get a bit tricky at this point, but I think I’m done now 😂 It all seems to work just fine.
As a side question, I see that the
consumedWindowInsets
is considered experimental now. Could you share why that is? Is the API surface still being considered as unstable, or are there any known issues with it or something else?
a
Glad to hear it! One of the tricky things in the area is
contentPadding
, and how to provide the insets in such a way that it respects the consumed insets: https://issuetracker.google.com/issues/237019262
WindowInsets.ime
will return the underlying, full insets values, and related to that
WindowInsets.ime.asPaddingValues()
won’t interact with
consumedWindowInsets
. There probably should be some way to interop there, either to get how many insets have been consumed so far (to use to subtract) or provide padding values that would interact with
consumedWindowInsets
.