Search code examples
androidwindowinsets

Go edge-to-edge on Android correctly with WindowInsets


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.


Solution

  • 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).