Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What exactly does fitsSystemWindows do?

System windows are the parts of the screen where the system is drawing either non-interactive (in the case of the status bar) or interactive (in the case of the navigation bar) content.

Most of the time, your app won’t need to draw under the status bar or the navigation bar, but if you do: you need to make sure interactive elements (like buttons) aren’t hidden underneath them. That’s what the default behavior of the android:fitsSystemWindows=“true” attribute gives you: it sets the padding of the View to ensure the contents don’t overlay the system windows.

https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec


it does not draw behind the system bar it kind of stretches behind the bar to tint it with the same colors it has but the views it contains is padded inside the status bar if that makes sense


In short, if you're trying to figure out whether to use fitsSystemWindows or not, there's Insetter library by Chris Banes (a developer from the Android team) which offers a better alternative to fitsSystemWindows. For more details let's see the explanation below.

There's a good article published by Android team in 2015 - Why would I want to fitsSystemWindows?. It well explains the default behavior of the attribute and how some layouts like DrawerLayout overrides it.

But, it was 2015. Back in 2017 at droidcon Chris Banes, who works on Android, advised not to use fitSystemWindows attribute unless a container documentation says to use it. And the reason for this is that the default behavior of the flag often doesn't meet your expectations. It's well explained in the video.

But what are these special layouts where you should use fitsSystemWindows? Well, it's DrawerLayout, CoordinatorLayout, AppBarLayout and CollapsingToolbarLayout. These layouts override the default fitsSystemWindows behavior and treat it in a special way, again it's well explained in the video. Such different interpretation of the attribute sometimes leads to a confusion and questions like here. Actually, in another video of droidcon London Chris Banes admits that the decision to overload the default behavior was a mistake (13:10 timestamp of the London conf).

Ok, if fitSystemWindows isn't the ultimate solution, what should be used? In another article from 2019 Chris Banes suggests another solution, a few custom layout attributes based on WindowInsets API. For example, if you want a bottom-right FAB to margin from the navigation bar, you can easily configure it:

<com.google.android.material.floatingactionbutton.FloatingActionButton
  app:marginBottomSystemWindowInsets="@{true}"
  app:marginRightSystemWindowInsets="@{true}"
  ... />

The solution uses custom @BindingAdapters, one for paddings and another for margins. The logic is well described in the article I've mentioned above. Some google samples use the solution, for example see Owl android material app, BindingAdapters.kt. I just copy the adapter code here for a reference:

@BindingAdapter(
    "paddingLeftSystemWindowInsets",
    "paddingTopSystemWindowInsets",
    "paddingRightSystemWindowInsets",
    "paddingBottomSystemWindowInsets",
    requireAll = false
)
fun View.applySystemWindowInsetsPadding(
    previousApplyLeft: Boolean,
    previousApplyTop: Boolean,
    previousApplyRight: Boolean,
    previousApplyBottom: Boolean,
    applyLeft: Boolean,
    applyTop: Boolean,
    applyRight: Boolean,
    applyBottom: Boolean
) {
    if (previousApplyLeft == applyLeft &&
        previousApplyTop == applyTop &&
        previousApplyRight == applyRight &&
        previousApplyBottom == applyBottom
    ) {
        return
    }

    doOnApplyWindowInsets { view, insets, padding, _ ->
        val left = if (applyLeft) insets.systemWindowInsetLeft else 0
        val top = if (applyTop) insets.systemWindowInsetTop else 0
        val right = if (applyRight) insets.systemWindowInsetRight else 0
        val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0

        view.setPadding(
            padding.left + left,
            padding.top + top,
            padding.right + right,
            padding.bottom + bottom
        )
    }
}

@BindingAdapter(
    "marginLeftSystemWindowInsets",
    "marginTopSystemWindowInsets",
    "marginRightSystemWindowInsets",
    "marginBottomSystemWindowInsets",
    requireAll = false
)
fun View.applySystemWindowInsetsMargin(
    previousApplyLeft: Boolean,
    previousApplyTop: Boolean,
    previousApplyRight: Boolean,
    previousApplyBottom: Boolean,
    applyLeft: Boolean,
    applyTop: Boolean,
    applyRight: Boolean,
    applyBottom: Boolean
) {
    if (previousApplyLeft == applyLeft &&
        previousApplyTop == applyTop &&
        previousApplyRight == applyRight &&
        previousApplyBottom == applyBottom
    ) {
        return
    }

    doOnApplyWindowInsets { view, insets, _, margin ->
        val left = if (applyLeft) insets.systemWindowInsetLeft else 0
        val top = if (applyTop) insets.systemWindowInsetTop else 0
        val right = if (applyRight) insets.systemWindowInsetRight else 0
        val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0

        view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
            leftMargin = margin.left + left
            topMargin = margin.top + top
            rightMargin = margin.right + right
            bottomMargin = margin.bottom + bottom
        }
    }
}

fun View.doOnApplyWindowInsets(
    block: (View, WindowInsets, InitialPadding, InitialMargin) -> Unit
) {
    // Create a snapshot of the view's padding & margin states
    val initialPadding = recordInitialPaddingForView(this)
    val initialMargin = recordInitialMarginForView(this)
    // Set an actual OnApplyWindowInsetsListener which proxies to the given
    // lambda, also passing in the original padding & margin states
    setOnApplyWindowInsetsListener { v, insets ->
        block(v, insets, initialPadding, initialMargin)
        // Always return the insets, so that children can also use them
        insets
    }
    // request some insets
    requestApplyInsetsWhenAttached()
}

class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int)

class InitialMargin(val left: Int, val top: Int, val right: Int, val bottom: Int)

private fun recordInitialPaddingForView(view: View) = InitialPadding(
    view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom
)

private fun recordInitialMarginForView(view: View): InitialMargin {
    val lp = view.layoutParams as? ViewGroup.MarginLayoutParams
        ?: throw IllegalArgumentException("Invalid view layout params")
    return InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
}

fun View.requestApplyInsetsWhenAttached() {
    if (isAttachedToWindow) {
        // We're already attached, just request as normal
        requestApplyInsets()
    } else {
        // We're not attached to the hierarchy, add a listener to
        // request when we are
        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

As you can see the realization isn't trivial. As I mentioned before, you're welcome to use Insetter library by Chris Banes which offers the same functionality, see insetter-dbx.

Also note that WindowInsets API is going to change since version 1.5.0 of androidx core library. For example insets.systemWindowInsets becomes insets.getInsets(Type.systemBars() or Type.ime()). See the library documentation and the article for more details.

References:

  • Why would I want to fitsSystemWindows?
  • WindowInsets — listeners to layouts
  • Animating your keyboard (part 1)
  • Becoming a master window fitter (droidcon London 2017)
  • Becoming a master window fitter (droidcon NYC 2017)