Image for post
Image for post

WindowInsets — Listeners to layouts

Moving where we handle insets to where our views live, layout files

Chris Banes
Apr 12, 2019 · 6 min read

If you’ve watched my Becoming a Master Window Fitter talk, you’ll know that handling window insets can be complex. Recently I’ve been improving system bar handling in a few apps, enabling them to draw behind the status and navigation bars. I’ve come up with some methods which make handling insets easier.

Drawing behind the navigation bar

For the rest of this post we will be going through a simple example using a , which is laid out at the bottom of the screen. It is very simply implemented as so:

<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent" />
Image for post
Image for post

By default, your Activity’s content will be laid out within the system provided UI (navigation bar, etc), thus our view sits flush to the navigation bar. Our designer has decided that they would like the app to start drawing behind the navigation bar though. To do that we’ll call with the appropriate flags:

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

And finally we will update our theme so that we have a translucent navigation bar, with dark icons:

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
<!-- Set the navigation bar to 50% translucent white -->
<item name="android:navigationBarColor">#80FFFFFF</item>
<!-- Since the nav bar is white, we will use dark icons -->
<item name="android:windowLightNavigationBar">true</item>
</style>
Image for post
Image for post
The view is being displayed behind the navigation bar

As you can see, this is only the beginning of what we need to do though. Since the Activity is now laid out behind the navigation bar, our is too. This means the user can’t actually click on any of the navigation items. To fix that, we need to handle any WindowInsets which the system dispatches, and use those values to apply appropriate padding to views.

Handling insets through padding

One of the usual ways to handle WindowInsets is to add padding to views, so that their contents are not displayed behind the system views. To do that, we can set an OnApplyWindowInsetsListener to add the necessary bottom padding to the view, ensuring that its content isn’t obscured.

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
Image for post
Image for post
The view now has bottom padding which matches the navigation bar size

OK great, we’ve now correctly handled the bottom system window inset. But later the designer decided to add some padding in the layout too:

<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp" />

Note: I’m not recommending using 24dp of vertical padding on a BottomNavigationView, I am using a large value here just to make the effect obvious.

Image for post
Image for post
The view has the correct top padding, but the intended bottom padding isn’t there

Hmmm, that’s not right. Can you see the problem? Our call to from the will now wipe out the intended bottom padding from the layout.

Aha! Lets just add the current padding and the inset together:

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(
bottom = view.paddingBottom + insets.systemWindowInsetsBottom
)
insets
}

We now have a new problem. WindowInsets can be dispatched at any time, and multiple times during the lifecycle of a view. This means that our new logic will work great the first time, but for every dispatch we’re going to be adding more and more bottom padding. Not what we want. 🤦

Image for post
Image for post
The accumulated padding after 3 WindowInset dispatches

The solution I’ve come up with is to keep a record of the view’s padding values after inflation, and then refer to those values instead. Example:

// Keep a record of the intended bottom padding of the view
val bottomNavBottomPadding = bottomNav.paddingBottom

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
// We've got some insets, set the bottom padding to be the
// original value + the inset value
view.updatePadding(
bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
)
insets
}
Image for post
Image for post
Finally, what we intended

This works great, and means that we maintain the intention of the padding from the layout, and we still inset the views as required. Keeping object level properties for each padding value is very messy though, we can do better… 🤔

doOnApplyWindowInsets

Enter my new extension method. This is a wrapper around which generalises the pattern above:

When we need a view to handle insets, we can now do the following:

bottomNav.doOnApplyWindowInsets { view, insets, padding ->
// padding contains the original padding values after inflation
view.updatePadding(
bottom = padding.bottom + insets.systemWindowInsetBottom
)
}

Much nicer! 😏

requestApplyInsetsWhenAttached()

You may have noticed the above. This isn’t strictly necessary, but does work around a limitation in how WindowInsets are dispatched. If a view calls while it is not attached to the view hierarchy, the call is dropped on the floor and ignored.

This is a common scenario when you create views in . The fix would be to make sure to simply call the method in instead, or use a listener to request insets once attached. The following extension function handles both cases:

Wrapping it up in a bind

At this point we’ve greatly simplified how to handle window insets. We are actually using this functionality in some upcoming apps, including one for an upcoming conference 😉. It still has some downsides though:

  • The logic lives away from our layouts, meaning that it is very easy to forget about.
  • We will likely need to use this in a number of places, leading to lots of near-identical copies spreading throughout the app.

I knew we could do better.

So far this entire post has concentrated solely on code, and handling insets through setting listeners. We’re talking about views here though, so in an ideal world we would declare our intention to handle insets in our layout files.

Enter data binding adapters! If you’ve never used them before, they let us map code to layout attributes (when you use Data Binding). You can read more about them here:

So lets create an attribute to do this for us:

In our layout we can then simply use our new attribute, which will automatically update with any insets.

<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp"
app:paddingBottomSystemWindowInsets="@{ true }" />

Hopefully you can see how ergonomic and easy to use this is compared to using an alone. 🌠

But wait, that binding adapter is hardcoded to only set the bottom dimension. What if we need to handle the top inset too? Or the left? Or right? Luckily binding adapters let us generalise the pattern across all dimensions really nicely:

Here we’ve declared an adapter with multiple attributes, each mapping to the relevant method parameter. One thing to note, is the usage of , meaning that the adapter can handle any combination of the attributes being set. This means that we can do the following for example, setting both the left and bottom:

<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp"
app:paddingBottomSystemWindowInsets="@{ true }"
app:paddingLeftSystemWindowInsets="@{ true }"
/>

Ease of use level: 💯

android:fitSystemWindows

You might have read this post, and thought “Why hasn’t he mentioned the fitSystemWindows attribute?”. The reason for that is because the functionality that the attribute brings is not usually what we want.

If you’re using AppBarLayout, CoordinatorLayout, DrawerLayout and friends, then yes use it. Those views have been built to recognize the attribute, and apply window insets in an opinionated way relevant to those views.

The default View implementation of means to pad every dimension using the insets though, and wouldn’t work for the example above. For more information, see this blog post which is still very relevant:

Ergonomics FTW

Phew, this was a long post! Aside from us making easier to handle, hopefully it has demonstrated how features like extension functions, lambdas, and binding adapters can make any API easier to use.

Android Developers

The official Android Developers publication on Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store