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.