Any good examples using windowinsets? I simply want to have vertical scrolling content showing benea...
m
Any good examples using windowinsets? I simply want to have vertical scrolling content showing beneath the navbar and with the appropriate bottom padding so that the bottom of the content can be scrolled into view. I’ve tried a bunch of ways without success. Quite frustrating. I also looked at the NIA app which is “edge-to-edge” but sadly doesn’t draw beneath any system bars, nor does it properly support imePadding (to check: open up the keyboard when showing search results (for “test”), and you cannot scroll to the bottom of the content).
s
What is it that doesn't work exactly for you? This is probably not what you're looking for, but we do have this super opinionated and limited scaffold we use https://github.com/HedvigInsurance/android/blob/develop/app%2Fcore%2Fcore-ui%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fcore%2Fui%2Fscaffold%2FHedvigScaffold.kt which for simple screens is honestly sufficient enough. Maybe it can be a good starting point for you?
m
When it comes to the LazyColumn that holds my vertically-scrolling content, the navBar insets have already been consumed, though I’ve no idea where. Thanks for the link, I’ll take a look shortly. I suppose one likely culprit, is that the LazyColumn is in a fragment (still using androidx navigation)
s
If they are already consumed that's something you need to fix higher up where you consume them really. Don't think a fragment itself would have something to do with it. Maybe whatever is containing your fragment?
m
It’s just a
Scaffold
at the top level where the navHost is a
FragmentContainerView
containing the fragment. The fragment UI is a compose
HorizontalPager
and my scrolling content is the first page. I really can’t see anything that’s consuming the insets. They are not consumed at the Scaffold level i.e. I’m able to do
navigationBarsPadding
with expected results (i.e. not drawn beneath navbars). I’m not doing that though, because I know that consumes the insets. I’ll keep looking. Thanks.
s
There are some functions that respect the consumed insets and some that just give you the value as is. Not sure which one navigationBarsPadding is. But you say you have a scaffold, why do you think the scaffold doesn't consume them? If you're passing a top app bar there you're I'd say definitely consuming them there.
m
If I don’t use
navigationBarsPadding
in the scaffold modifier then the scrollable content appears beneath the navbar, as expected. Are you saying that if I don’t do
navigationBarsPadding
the scaffold will consume the navBar insets without doing anything with them?
t
I think it is very difficult to get edge-to-edge support right. I am currently writing an article about that. If you want i could provide you a prerelease version. There i go into some common problems. Just send me a PM if you are interested.
Regarding the LazyColumn: There is a problem because you can not use a modifier to provide the insets to the LazyColumn. It must be provided as PaddingValues. And when you request the insets using WindowInsets api the consumed insets are ignored. So here is a workaround to get the insets as padding values respecting the consumed insets:
Copy code
@Composable
fun insetsPaddingValues(insets: WindowInsets): PaddingValues {
    val density = LocalDensity.current
    var paddingValues by remember { mutableStateOf(insets.asPaddingValues(density)) }
    Spacer(modifier = Modifier.onConsumedWindowInsetsChanged(
        block = {
            paddingValues = insets.exclude(it).asPaddingValues(density)
        }
    ))
    return paddingValues
}
s
Yep we're doing the same little dance for the consumed insets here https://github.com/HedvigInsurance/android/blob/develop/app%2Ffeature%2Ffeature-profile%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Ffeature%2Fprofile%2Ftab%2FProfileDestination.kt#L182-L192 Your convenience function which places a Spacer composable but also returns a value is kinda like breaking every compose convention out there, but I wonder if there is a better way to do this in the first place 😅
t
Copy code
val contentPadding = insetsPaddingValues(WidnowInsets.safeDrawing)
LazyColumn(
    contentPadding = contentPadding
) {
    // list content items
}
s
Yeah I was mostly worried about a composable with a small starting letter which also places a UI element on the screen, and also returns a value, when I said breaking conventions.
t
Yea you are right. I know it is bad. 😄 But no official solution yet i think.
s
With all that said, I am sure I am missing something, but after setting up some things right, like the top app bar consuming the insets, the bottom nav bar doing the same and so on, on a single activity compose only app I haven't really found it too much to support edge to edge. I am really looking forward to reading your blog because I want to see what kind of things I am perhaps not doing.
t
If you do have a very simple screen it is easy with compose. But as soon as you do have multiple components on the screen and a LazyList and maybe also want to support landscape it gets more and more complex. Also this inset consumptions only works for downstream composables. So if you do have a screen split into main/detail or content/appbar you do have to code a layoutmanager which does this insets consumtion calculation.
Maybe it helps someone here is a layout component i am using in an App to show main content and AppBar with landscape support and windowinsets:
Copy code
@Composable
fun MainLayout(
    modifier: Modifier,
    mainContent: @Composable () -> Unit,
    toolbarItem: @Composable (Modifier, NavIcon, Orientation) -> Unit,
) {
    val main = remember(mainContent) { movableContentOf(mainContent) }
    val toolbar = remember(toolbarItem) { movableContentOf(toolbarItem) }
    BoxWithConstraints {
        val isPortrait = maxWidth < maxHeight
        if (isPortrait) {
            Column(modifier.fillMaxSize()) {
                Box(
                    Modifier
                        .weight(1f)
                        .fillMaxWidth()
                        .consumeWindowInsets(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
                ) {
                    main()
                }
                Row(
                    Modifier
                        .fillMaxWidth()
                        .background(MaterialTheme.colors.navigationBarColor)
                        .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))
                        .height(64.dp)
                ) {
                    NavIcon.entries.forEach { icon ->
                        Box(Modifier.weight(1f)) {
                            toolbar(Modifier.align(Alignment.Center), icon, Orientation.Horizontal)
                        }
                    }
                }
            }
        } else {
            Row(modifier.fillMaxSize()) {
                Column(
                    modifier = Modifier
                        .fillMaxHeight()
                        .background(MaterialTheme.colors.navigationBarColor)
                        .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Start + WindowInsetsSides.Vertical))
                        .width(100.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    NavIcon.entries.forEach { icon ->
                            toolbar(
                                Modifier
                                    .weight(1f)
                                    .fillMaxWidth(), icon, Orientation.Vertical)
                    }
                }
                Box(
                    Modifier
                        .weight(1f)
                        .fillMaxHeight()
                        .consumeWindowInsets(WindowInsets.safeDrawing.only(WindowInsetsSides.Start))
                ) {
                    main()
                }
            }
        }

    }
}
👍 1
s
Is toolbar in this context the bottom nav bar or the nav rail on the left in portrait mode?
This also uses BoxWithConstraints, therefore subcomposition, and you do that to get if you are portrait or not. But you also check that by simply seeing what is bigger than the other right? Which isn't necessarily true when you got apps side-by-side and other such scenarios. Isn't the WindowSizeClass what you should be using here instead to avoid subcomposition and also get the proper size without making assumptions for portrait/landscape mode?
What we do is just this here https://github.com/HedvigInsurance/android/blob/develop/app%2Fapp%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fapp%2Fui%2FHedvigApp.kt#L75-L173 this is above the NavHost so from that point and down all the child composables just get the right consumed insets depending on where the NavRail/BottomNav shows, and if they show at all. The screens themselves just use the right inset apis and you're good. If using the apis that don't respect the consumed insets, then you gotta do the trick showed above, reading the consumed insets and excluding those from your PaddinValues for example.
m
Regarding checking for landscape, can’t you just use the code below? If you are in portrait orientation, but split screen, then the returned value will adjust accordingly
Copy code
@Composable
@ReadOnlyComposable
fun isLandscape(): Boolean = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
Of course, this is only useful if
MainLayout
is your whole UI.
s
That again doesn't say much. What if you are in landscape but you are in split app mode, therefore your app is more tall than wide? Again, window size class is your solution there, this article https://medium.com/androiddevelopers/jetnews-for-every-screen-4d8e7927752 touches on this too.
m
I'm just mentioning that out of interest. The above
isLandscape()
will return true if the device is in portrait and the split screen is small enough to be in landscape.
s
I'm not sure what you mean here
m
When you split a portrait screen so you have top and bottom apps, it's possible you now have one or both apps in landscape.
isLandscape()
returns true according to the space available to the app, not the orientation of the device.
s
You mean that this https://developer.android.com/reference/android/content/res/Configuration#ORIENTATION_LANDSCAPE considers the current app's screen size as opposed to just giving you if the orientation of the phone is landscape or not?
m
Yes, at least that’s my experience. As I change the size of the app in split screen, I get a different value from
_LocalConfiguration_.current.orientation
accordingly, without any change in the device orientation. Are you seeing something different?
s
I can't test as I am afk now but yeah it seems like I misunderstood that flag 👀
m
Last time I looked at this was several years ago when split screen functionality was introduced. AFAIR, the
Display
size was also adjusted accordingly.
s
With that said, to avoid the XY problem here, our goal wasn't necessarily for find if our width is more than the height, but when to show the bottom nav bar instead of the nav rail on the left. The decision for that is not if the width is more than the height alone, but depending on how much the width is itself. In a really big screen, where the width in window size class in not considered Compact, but where the height is also very tall, yet still taller than the width, you're still expected (by m3 specs) to show the nav rail then, despite being in "portrait mode" by the definition you describe here. See https://m3.material.io/components/navigation-rail/overview and https://m3.material.io/foundations/layout/applying-layout/medium
👍 1
t
My goal is often also keep the code complexity as low as possible. And yes i experimented with sizeclasses and also i did testing with a foldable device and with tablets. But yea all in all it is very complex. My content view is also dynamically split into two screens. And there i also try to place things according to the fold of foldable devices. But yea it is not perfect.
I just thought a littlebit about my code and i should maybe also substract the insets from the calculation.
m
Yeah my only point is that if you are only using boxconstraint to compare maxwidth and maxheight, and that is the top level of your UI, then you are better off using isLandscape()
t
When i remember correctly this did not work well for the Pixel Fold device. But not sure anymore. At the end i think it is better to check if the layout fits best using the real size values. But at the end it depends what you want. I think the best would be also to maybe fold the appbar when a detail view is shown. But not sure about that. Currently i added a feature to make the detail view full size of the content area. Maybe i should enforce this when the width for the list view is too small.
I just published an article about the problems and complexity when supporting edge-to-edge design. Maybe someone is interested here: https://github.com/timo-drick/compose_libraries/tree/main/edge_to_edge_preview_lib
👍 1
367 Views