WindowInsets

Android 11: WindowInsets

Getting Window Insets working correctly can be tricky. It’s a subject that we’ve looked at before on Styling Android and we covered some of the complexity involved in that article. With the release of the developer previews of Android 11 there is a new API available for handling Window Insets and they are good! In this article we’ll look at these new APIs and see how much easier things have become.

Before we dive in to the code, there’s a couple of things worth mentioning.

Firstly, these are not final APIs and they may still change. The sample code has been verified against Android 11 Developer Preview 2.

Secondly, the new APIs are currently only available in Android 11 and that will be of little use to the majority of developers who will be unable to use them for a few years until they can minSdkVersion="30". However, Chris Banes has tweeted that work is ongoing for and AndroidX version of these new APIs but it won’t be available until the Android 11 SDKs are final:

The first big change is that prior to Android 11 it is necessary to set system UI visibility flags in order to get our layout to draw behind the system controls such as the status and navigation bars:

window.decorView.systemUiVisibility =
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

If you do this when targeting Android 11 you’ll get warnings that systemUiVisibility, SYSTEM_UI_FLAG_LAYOUT_STABLE, and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION are all deprecated. This was always a pretty cumbersome mechanism because there were a number of flags that could be combined here, and knowing which ones would get the desired behaviour always required a bit of trial and error. Also, this needed to be done before inflating our layout otherwise it had no effect.

The replacement for this is much easier to use, and far more flexible:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        window.setDecorFitsSystemWindows(false)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        .
        .
        .
    }
    .
    .
    .
}

The new setDecorFitsSystemWindows() method does everything we need, and is much easier to remember! The single argument controls whether or not our layout will fit inside the system windows (if true), or be draw behind them (if false). For these articles we’re only going to consider the case where window.setDecorFitsSystemWindows(false) is set – as this is the most common use-case and things get a little more complex otherwise (perhaps we’ll re-visit the other use-case in a future article).

The other useful thing about setDecorFitsSystemWindows() is that we are no longer required to call it before inflating out layout – in fact we can toggle it at will, and the sample app shows this. However when it is set to true all of the window insets handling is disabled as previously discussed.

The sample app looks like this:

The background consists of an X which fills the area. As we adjust the margins to allow for window insets we’ll see changes to this which show the insets being applied. Currently it stretches to all four corners of the display because no insets have been applied.

There’s a CheckBox at the top which toggles the setDecorFitsSystemWindows() state that we just looked at. This is followed by a series of 7 CheckBoxes which represent each of the inset types that we can obtain – we’ll explore these more in a moment, and finally there’s an EditText which we can use to display the IME. Let’s look at the code for this:

class MainActivity : AppCompatActivity() {

    private val currentInsetTypes = mutableSetOf()
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        window.setDecorFitsSystemWindows(false)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.root.setOnApplyWindowInsetsListener { _, _ ->
            applyInsets()
        }

        val itemTypes = listOf(
            binding.toggleCaptionBar,
            binding.toggleIme,
            binding.toggleMandatorySystemGestures,
            binding.toggleStatusBars,
            binding.toggleSystemBars,
            binding.toggleSystemGestures,
            binding.toggleTappableElement
        )

        binding.toggleDecorFitSystemWindows.setOnCheckedChangeListener { _, checked ->
            window.setDecorFitsSystemWindows(checked)
            binding.insetTypes.isEnabled = checked
            if (checked) {
                itemTypes.forEach { checkBox ->
                    checkBox.isChecked = false
                    checkBox.isEnabled = false
                }
                currentInsetTypes.clear()
            } else {
                itemTypes.forEach { checkBox ->
                    checkBox.isEnabled = true
                }
            }
        }
        binding.toggleCaptionBar.addChangeListener(Type.captionBar())
        binding.toggleIme.addChangeListener(Type.ime())
        binding.toggleMandatorySystemGestures.addChangeListener(Type.mandatorySystemGestures())
        binding.toggleStatusBars.addChangeListener(Type.statusBars())
        binding.toggleSystemBars.addChangeListener(Type.systemBars())
        binding.toggleSystemGestures.addChangeListener(Type.systemGestures())
        binding.toggleTappableElement.addChangeListener(Type.tappableElement())
    }

    private fun CheckBox.addChangeListener(type: Int) =
        setOnCheckedChangeListener { _, checked -> toggleType(type, checked) }

    private fun toggleType(type: Int, required: Boolean) {
        if (required) {
            currentInsetTypes.add(type)
        } else {
            currentInsetTypes.remove(type)
        }
        applyInsets()
    }
    .
    .
    .
}

Most of this code is concerned with the UI itself, and I’m not going to do a deep dive in to all of this code because it really is not relevant to the subject at hand, but the relevant bits are those highlighted. We’ve already discussed the toggling of the setDecorFitsSystemWindows() state (lines 9 & 28). There is a set of the WindowInset.Type values that are currently selected that gets stored in currentInsetTypes and this is maintained in lines 3, 35, 56, and 58. The other thing that we need to do is add a WindowInsetsListener (lines 13-16) which will be called whenever the insets are changed by the system, such as when the IME is shown or hidden. The workhorse for applying the insets is the applyInsets() method which is called from lines 14 and 60:

class MainActivity : AppCompatActivity() {
    .
    .
    .
    private fun applyInsets(): WindowInsets {
        val currentInsetTypeMask = currentInsetTypes.fold(0) { accumulator, type ->
            accumulator or type
        }
        val insets = binding.root.rootWindowInsets.getInsets(currentInsetTypeMask)
        binding.root.updateLayoutParams {
            updateMargins(insets.left, insets.top, insets.right, insets.bottom)
        }
        return WindowInsets.Builder()
            .setInsets(currentInsetTypeMask, insets)
            .build()
    }
}

There is a lot happening in a few lines, so let’s go though this in detail.

We first of all need to create a type mask which is the WindowInset.Type values that we’re interested in or-ed together. We achieve this my using a Koltin fold operator on the set of WindowInset.Types in currentInsetTypes. This works by using an accumulator value which is initialised as 0 in the argument to fold(0), and then the lambda is called for each item in the set. In the lambda we get two arguments the accumulator (which is the running total), and the type which is the set item being evaluated. We or the accumulator with type and a new value is returned and this is now assigned to the accumulator by the fold operator. This new accumulator value will be passed to the lambda for the next item. This basically ors all of the values in the set together.

Next we get the insets for this type mask and store this in the val insets.

Next we apply these insets to the current layout. I am using the KTX extensions updateLayoutParams and updateMargins to achieve this.

Finally we build and return a WindowInsets instance from currentInsetTypeMask and insets. The WindowInsetsListener needs to return a WindowInsets instance, and this provides that.

So let’s now look at the seven WindowInset.Type values and what they represent.

Type.captionBar()

This is mainly used for the ‘live caption’ subtitle bar, and will be mainly of use in media player apps and suchlike (many thanks to Chris Banes for the info on this as I was unable to work it out without his explanation).

Type.ime()

This gets insets for the IME. When the IME is displayed, a large bottom inset will be included in the insets. This is incredibly useful! Note how the cross in the background changes to match the space remaining once the IME is displayed:

Type.mandatorySystemGestures()

This gets the insets for gestures which are mandated by the Android Framework. Currently these are the swipe down on the status bar at the top of the screen, and the swipe up from the bottom of the screen if gesture navigation is enabled on the device. Note how the background X is no longer displayed behind those elements. Although many apps would still want to display behind the system controls, the gesture insets can be used to ensure that app gesture do not encroach on these areas.

Chris informs me this should only be the bottom navigation and there may be a bug that is causing the insets for the status bar to be incorrectly included here. This behaviour was observed in Android 11 Developer Preview 2, but this may change in future version.

Type.statusBars()

This gets the insets for the status bar. The background X is not drawn behind the status bar, but is still drawn behind the navigation bar at the bottom of the screen.

Type.systemBars()

This gets the insets for all system bars – the status bar, caption bar, and navigation bar – in one pass. The background X is not drawn behind the status bar or the navigation bar in this example. While this appears to be identical to the mandatory system gestures this is only because those gesture zones happen to coincide with these two bars – what the insets represent is actually different and will typically be used by apps in different ways – use the gesture insets to ensure that app gestures don’t interfere; and use system bar insets to ensure that we don’t draw anything behind the system, bars that may make them difficult to see.

Type.systemGestures()

This gets the insets for all system gestures including the mandatory ones we discussed earlier, and any that can be suppressed by the app such as the left and right bezel swipes used for back navigation when gesture navigation is enabled on the device. It is possible to suppress these in cases where they may interfere with the operation of controls like navigation drawers, hence the reason they are not included in the mandatory gestures.

Type.tappableElement()

This gets the insets for any system controls which are tappable – i.e. they consume tap / click events, and not just gestures. Currently this is just the system status bar which can be tapped to expand it. One again, although this may seem identical to statusBars(), it actually represents something different and can be used to ensure that we don’t display controls which themselves require touch input behind or even too close to those system controls.

The key to using these different inset types is to understand what they all represent, and use the correct ones that are appropriate to the behaviours of your app. By doing so you will get compatibility if ever the system behaviour change in subtle ways in the future.

Many thanks to Chris Banes not only for reviewing and providing information and feedback for this article, but also the great work he’s done in terms of code, tools, and documentation around the whole area of WindowInsets.

The source code for this article is available here.

© 2020, Mark Allison. All rights reserved.

Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.