Go edge-to-edge on Android correctly with WindowInsets
Asked Answered
C

2

5

I'm trying to get edge-to-edge (https://youtu.be/OCHEjeLC_UY?t=1635) working correctly on API 21 up to 29.

I'm using this on my v27\themes.xml:

<item name="android:windowLightNavigationBar">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>

And this in my Activity:

override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)

    if (hasFocus) {
        window.decorView.systemUiVisibility =
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    }
}

Plus, I'm setting android:fitsSystemWindows=true on my AppBarLayout.


With that in place, it looks fine on API >= 27 where the content scrolls behind the now transparent navigation bar but on older APIs, the content is covered by the black navigation bar.

I know that I need to get the WindowInsets and add it to my existing padding (or in case of AppBarLayout it will handle the insets itself), but I cannot get it to work with a FAB.

I found this article about adding the inset to the padding of a view but because the FAB uses margin I'm not sure, if I'm on the right track.


Is there any documentation, example, best practice on how to handle insets when going edge-to-edge? It seems that some widgets like AppBarLayout handle it gracefully but how can I get the FAB to adjust its margin as well?


Update 1

To specify, when adding android:fitsSystemWindows=true to CoordinatorLayout it handles the insets as well but with a major drawback:

I have two layouts, each with a CoordinatorLayout: "parent layout" defines a CoordinatorLayout with a AppBarLayout and a FrameLayout to hold the actual content and "child layout" used by a Fragment placed in the latter.

Because of this, I cannot add android:fitsSystemWindows=true to the child layout because it would result in a blank space on top (between the toolbar and the content) and I cannot put it in the parent layout because then the FAB won't be updated to the insets.

Curran answered 31/7, 2019 at 14:44 Comment(8)
That article by banes is the holy grail for window insets. You don't want to adjust the margin of the FAB, rather you want to adjust the padding of the layout that contains the FABOtto
I avoid android:fitsSystemWindows at all costs because it works very differently depending on the view you use it on. Using banes' insets and manually adding that to views you want is much easier, in my opinionOtto
@TimCastelijns that's the correct way to think about it, thanks! But I now have to add a container around the FAB to do so because I want other content to scroll behind the navigation bar. My final solution is now to add doOnApplyWindowInsets() adding bottom-insets to the FAB-container and the container holding my content inside the ScrollView (but not the ScollView itself). Adding fitsSystemWindows to the AppBarLayout handles the top-insets.Curran
I still have trouble with the FAB. Adding a container around it to apply the padding breaks moving the FAB upwards when a Snackbar is shown (probably because the FAB is no longer a direct child of the CoordinatorLayout). How can I add the inset-padding to the FAB correctly (I still want the content to be scrolled behind the navigation bar and therefore cannot add the inset to the CoordinatorLayout).Curran
I think you can remove the container around the FAB and instead of how banes updates the padding when the insets change, you update the FAB marginOtto
Is there a corresponding method on View for margin (just like View.updatePadding())?Curran
Here we go: Set margin via KTX's View.updateLayoutParams() by casting the provided layoutParams in the lambda with (this as? ViewGroup.MarginLayoutParams) and then using ViewGroup.MarginLayoutParams.updateMargins() to update the bottom margin.Curran
Mostly done but still one problem: When showing a snackbar, the FAB moves up thanks to CoordinatorLayout. But because it now has much more margin, it moves up too much. Is there a solution as well?Curran
C
19

Edit 2022-01-16:

Nowadays we can use https://google.github.io/accompanist/insets to get insets for jetpack compose and just add WindowCompat.setDecorFitsSystemWindows(window, false) like @shogun-nassar points out correctly in his answer.


Original answer:

To provide a final answer:

Do not use android:fitsSystemWindows anywhere but apply insets manually to any view at the edge of the screen which would otherwise slip behind the system bars (e.g. AppBarLayout or FloatingActionButton).

I wrote some helpers to add the insets to either padding or margin, respecting any previously added one (needs androidx.core:core:1.2.0-alpha01):

fun View.addSystemWindowInsetToPadding(
    left: Boolean = false,
    top: Boolean = false,
    right: Boolean = false,
    bottom: Boolean = false
) {
    val (initialLeft, initialTop, initialRight, initialBottom) =
        listOf(paddingLeft, paddingTop, paddingRight, paddingBottom)

    ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
        view.updatePadding(
            left = initialLeft + (if (left) insets.systemWindowInsetLeft else 0),
            top = initialTop + (if (top) insets.systemWindowInsetTop else 0),
            right = initialRight + (if (right) insets.systemWindowInsetRight else 0),
            bottom = initialBottom + (if (bottom) insets.systemWindowInsetBottom else 0)
        )

        insets
    }
}

fun View.addSystemWindowInsetToMargin(
    left: Boolean = false,
    top: Boolean = false,
    right: Boolean = false,
    bottom: Boolean = false
) {
    val (initialLeft, initialTop, initialRight, initialBottom) =
        listOf(marginLeft, marginTop, marginRight, marginBottom)

    ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
        view.updateLayoutParams {
            (this as? ViewGroup.MarginLayoutParams)?.let {
                updateMargins(
                    left = initialLeft + (if (left) insets.systemWindowInsetLeft else 0),
                    top = initialTop + (if (top) insets.systemWindowInsetTop else 0),
                    right = initialRight + (if (right) insets.systemWindowInsetRight else 0),
                    bottom = initialBottom + (if (bottom) insets.systemWindowInsetBottom else 0)
                )
            }
        }

        insets
    }
}

As an example, simply call fab.addSystemWindowInsetToMargin(bottom = true) and the FAB will be moved up above the navigation bar. Or app_bar.addSystemWindowInsetToPadding(top = true) to keep the app bar below the status bar (mind the margin/padding difference).

Curran answered 2/8, 2019 at 12:31 Comment(2)
thank you!. I had a similar problem and could solve it with by updating margin rather then update padding.Hepta
Excellent answer! though insets.systemWindowInset methods are now deprecated and insets.getInsets(systemBars) should be used insteadTallowy
H
2

As per https://developer.android.com/training/gestures/edge-to-edge#kotlin

Just use this on BaseActivity or any activity you want:

   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     WindowCompat.setDecorFitsSystemWindows(window, false)
   }

N.B: Do not use anywhere this

android:fitsSystemWindows="true"
Hue answered 14/1, 2022 at 17:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.