WindowInsets

WindowInsetsCompat

In March I wrote about some extremely nice updates to WindowInsets that were introduced in the Android 11 developer preview. I mentioned in that article that a Jetpack version would be coming for backwards compatibility. This arrived in androidx.core 1.5.0-alpha02. In this article we’ll look at how to use these new Jetpack API.

I have already explored these APIs in the previous article. Rather than simply repeating that, this article will, instead, focus on how to switch the code from that article from being minSdkVersion="30" to minSdkVersion="21". The new APIs actually work back to API 16, but let’s not over-complicate things! API 21 will be fine for most apps.

I needed to make some changes to the code simply to get it running on older devices. For example, my launcher icon was using API 26 APIs. So I needed to make these compatible back to API 21. The first change relevant to these new APIs was to change the version of Core-Ktx to 1.5.0-alpha02. This implicitly adds a dependency upon the same version of androidx.core.

Functional Change

I needed to make one functional change. Previously I was calling getRootWindowInsets() on the root view of my layout to obtain the WindowInsets in my applyInsets() method. This method first appeared in API 23, so we cannot use it here. I was forced to adapt things slightly because there is no way to directly request the WindowInsets in API 22 and earlier. Instead we need to register an OnApplyInsetsListener which receives a callback when the insets change. I was actually doing this already, but not actually doing anything with the insets argument to the callback method. Instead I was explicitly requesting them in the applyInsets() method. In hindsight this wasn’t great code because it was doing work that it didn’t have to.

To fix this I have introduced a field named currentWindowInsets:

private var currentWindowInsets: WindowInsetsCompat = WindowInsetsCompat.Builder().build()

It is initialised with an empty WindowInstetsCompat instance for null safety – this will get replaced once we get the callback. The reason for this is that we call applyInsets() in response to two separate events: First, when the insets change and our listener receives a callback. Second, if the user toggles one of the check boxes which control which insets will be applied. In this latter case, we don’t receive the insets, so we need to use the one stored in currentWindowInsets. That specific case was the reason for performing the explicit lookup each time. However, storing the insets works perfectly.

Compat changes

The majority of the changes are switching from the Android 11 framework APIs to the androidx.core ones. Let’s begin with our onCreate() method:

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

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

        ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
            currentWindowInsets = windowInsets
            applyInsets()
        }

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

        binding.toggleDecorFitSystemWindows.setOnCheckedChangeListener { _, checked ->
            WindowCompat.setDecorFitsSystemWindows(window, 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())
    }

Previously we had:

window.setDecorFitsSystemWindows(true)

That has now been replaced with the WindowCompat equivalent. We do the same with setOnApplyWindowInsetsListener on the View using the ViewCompat equivalent.

The other changes are in our applyInsets() method:

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

We change the method return type to WindowInsetCompat when previously it was WindowInsets. We also use currentWindowInsets rather than explicitly requesting the current insets as we did previously. For the return value we construct a WindowInsetsCompat instance which is pretty much identical to how we were creating a WindowInsets instance before.

If we run this on an API 21 device we get the expected behaviour:

Conclusion

I’m already a fan of these new APIs for handling WindowInsets – they make life significantly easier, and us devs have needed them for a long time. With this new Jetpack release we can actually start using them in our apps!

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.