https://kotlinlang.org logo
Title
s

Stylianos Gakis

08/16/2022, 10:00 PM
If I got a structure like this:
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
@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

Francesc

08/16/2022, 10:09 PM
why not make this a
Column
with the scrollable content having a weight?
s

Stylianos Gakis

08/16/2022, 10:14 PM
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

Francesc

08/16/2022, 10:21 PM
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

Stylianos Gakis

08/16/2022, 10:26 PM
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

Francesc

08/16/2022, 10:27 PM
ok, so an option may be to use a custom
Layout
then
s

Stylianos Gakis

08/16/2022, 10:30 PM
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

Francesc

08/16/2022, 10:32 PM
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

Stylianos Gakis

08/16/2022, 10:56 PM
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

Alex Vanyo

08/16/2022, 11:25 PM
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

Stylianos Gakis

08/17/2022, 12:13 AM
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

Alex Vanyo

08/17/2022, 12:22 AM
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:
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

Stylianos Gakis

08/17/2022, 8:37 AM
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):
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.
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
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

Alex Vanyo

08/17/2022, 3:54 PM
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
.