If you’ve seen my Becoming a Master Window Fitter conversation, you know that dealing with Window plug-ins can be complicated. Recently, I’ve been working on improving the system bar handling in several applications so that they can draw behind the status and navigation bars. I think I’ve suggested some ways to make handling inserts easier (hopefully). The original
Draw behind the navigation bar
For the rest of this article, we’ll do a simple example using BottomNavigationView, located at the bottom of the screen. The implementation is very simple:
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent" />
Copy the code
By default, the content of your Activity is laid out in the system-provided UI(navigation bar, etc.), so our view is flush with the navigation bar. Our designers decided they wanted the application to start drawing behind the navigation bar. To do this, we’ll call setSystemUiVisibility() with the appropriate flag:
rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
Copy the code
Finally we will update our theme so that we have a translucent navigation bar with black 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>
Copy the code
As you can see, this is just the beginning of what we need to do. Since the activity is now behind the navigation bar, so is our BottomNavigationView. This means that the user cannot actually click on any navigation items. To solve this problem, we need to deal with any WindowInsets of system scheduling and apply appropriate padding or margins to the view using these values.
Handling inserts by padding
One of the common ways to deal with WindowInsets is to add padding to views so that their contents do not appear behind the System-UI. To this end, we can set OnApplyWindowInsetsListener, add necessary to view the bottom of the filling, to ensure the content is not obscured.
bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
Copy the code
Ok, we have now handled the insertion of the bottom system window correctly. But then we decided to add some padding to the layout, probably for aesthetic reasons:
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp" />
Copy the code
Note: I’m not using 24dp of vertical padding on a BottomNavigationView, I am using a large value here just to make the effect obvious.
Well, that’s not right. Can you see the problem? We call from OnApplyWindowInsetsListener updatePadding () will now be eliminate expected at the bottom of the filling from the layout.
Aha! Let’s add the current fill and insert together:
bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(
bottom = view.paddingBottom + insets.systemWindowInsetsBottom
)
insets
}
Copy the code
We now have a new problem. WindowInsets can be scheduled at _any_, and _multiple_ can be scheduled during the view’s life cycle. This means that our new logic will work fine on the first run, but with each subsequent schedule, we’ll add more and more bottom-padding. It’s not what we want. 🤦
The solution I came up with was to record the view’s fill-in values after inflation and then refer to those values. Ex. :
// 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
}
Copy the code
This works well and means that we keep the intention of populating from the layout, and we still insert views as needed. Keeping the object level attributes of each fill value is very messy and we can do better…… 🤔
doOnApplyWindowInsets
Enter the doOnApplyWindowInsets() extension method. This is [setOnApplyWindowInsetsListener ()] (developer.android.com/reference/a…
fun View.doOnApplyWindowInsets(f: (View.WindowInsets.InitialPadding) - >Unit) {
// Create a snapshot of the view's padding state
val initialPadding = recordInitialPaddingForView(this)
// Set an actual OnApplyWindowInsetsListener which proxies to the given
// lambda, also passing in the original padding state
setOnApplyWindowInsetsListener { v, insets ->
f(v, insets, initialPadding)
// Always return the insets, so that children can also use them
insets
}
// request some insets
requestApplyInsetsWhenAttached()
}
data class InitialPadding(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)
Copy the code
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
)
}
Copy the code
Much better! 😏
requestApplyInsetsWhenAttached()
You may have noticed the above requestApplyInsetsWhenAttached (). This is not absolutely necessary, but it does address the way WindowInsets are dispatched. If the view calls requestApplyInsets() when it is not attached to the view hierarchy, the call is left on the floor and ignored.
This is in the [fragments onCreateView ()] (developer.android.com/reference/a… Repair method is to ensure that simply call [onStart ()] (developer.android.com/reference/a… The following extension functions handle two cases:
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 : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit}}})Copy the code
Wrap it in a binding
At this point, we have greatly simplified how to handle window inserts. We actually use this feature in some upcoming applications, including the upcoming conference apps. It still has some disadvantages. First, logic is far removed from our layout, which means it is easy to forget. Second, we may need to use it in many places, resulting in a large number of near-identical copies being propagated throughout the application. I know we can do better.
So far, the entire post has focused only on code and handled insets by setting up listeners. We are talking about views here, so in an ideal world we would declare that we intend to deal with illustrations in the layout file.
Enter Data Binding Adapters! If you’ve never used them before, they let us map code to layout properties (when you use data binding). So, let’s create a property for us:
@BindingAdapter("paddingBottomSystemWindowInsets")
fun applySystemWindowBottomInset(view: View, applyBottomInset: Boolean) {
view.doOnApplyWindowInsets { view, insets, padding ->
val bottom = if (applyBottomInset) insets.systemWindowInsetBottom else 0
view.updatePadding(bottom = padding.bottom + insets.systemWindowInsetBottom)
}
}
Copy the code
In our layout, we can simply use our new paddingBottomSystemWindowInsets attribute, this property will automatically update any inserts.
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp"
app:paddingBottomSystemWindowInsets="@{ true }" />
Copy the code
Hopefully you can see how ergonomic and easy to use it is compared to OnApplyWindowListener alone. 🌠
But wait, the binding adapter hardcodes only the bottom size. What if we also need to deal with the top illustration? Or left? Is that still right? Fortunately, the binding adapter gives us a good overview of patterns for all dimensions:
@BindingAdapter(
"paddingLeftSystemWindowInsets"."paddingTopSystemWindowInsets"."paddingRightSystemWindowInsets"."paddingBottomSystemWindowInsets",
requireAll = false
)
fun applySystemWindows(
view: View,
applyLeft: Boolean,
applyTop: Boolean,
applyRight: Boolean,
applyBottom: Boolean
) {
view.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
)
}
}
Copy the code
Here we have declared an adapter with multiple properties, each of which maps to the relevant method parameters. One thing to note is the use of requireAll = false, which means that the adapter can handle any combination of properties you set. This means we can do the following, such as set left and bottom:
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp"
app:paddingBottomSystemWindowInsets="@{ true }"
app:paddingLeftSystemWindowInsets="@{ true }" />
Copy the code
Ease of use: 💯
Android: fitSystemWindows
You may have read this article and thought to yourself, “Why Hasn’t he Mentioned the fitSystemWindows attribute?” _. The reason for this is that attributes often don’t give us what we want.
If you are using AppBarLayout, CoordinatorLayout, DrawerLayout and Friends, follow the instructions. These views are built to identify properties and apply window inserts in a fixed way that is relevant to these views.
The default View implementation for android:fitSystemWindows means that insets are used to populate each dimension, but does not apply to the example above. For more information, see this blog post, which is still very relevant.