Windows Insets - Layout Monitor

Posted by sliilvia on Sun, 25 Aug 2019 13:12:36 +0200

If you've seen me already Becoming a Master Window Fitter Speaking, you'll know that processing window plug-ins can be complex. Recently, I have been improving the system bar processing in several applications so that they can draw behind the status and navigation bar. I think I've come up with some ways to make it easier to handle inserts (hopefully). original text

Draw behind the navigation bar

For the rest of this article, we will use BottomNavigationView Let's do a simple example, which is at the bottom of the screen. Its implementation is very simple:

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

By default, the content of your Activity will be laid out in the UI (navigation bar, etc.) provided by the system, so our view will be level with the navigation bar. Our designers decided that they wanted the application to start drawing behind the navigation bar. To do this, we will call it with the appropriate flag setSystemUiVisibility()):

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

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>

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 Bottom Navigation View. This means that the user cannot actually click on any navigation item. To solve this problem, we need to process any of the systems scheduled. WindowInsets And use these values to apply appropriate padding or margins to the view.

Handling inserts by filling

Handle WindowInsets One of the common methods is to add padding for views so that their contents do not appear after the system-ui. To do this, we can set up OnApplyWindowInsetsListener To add the necessary bottom padding to the view to ensure that its content is not obscured.

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
    view.updatePadding(bottom = insets.systemWindowInsetBottom)
    insets
}

Well, we have now correctly handled the insertion of the bottom system window. But then we decided to add some filling in the layout, probably for aesthetic reasons:

<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.

Well, that's not right. Can you see the problem? We call it from OnApply Windows InsetsListener updatePadding() The expected bottom filling will now be eliminated from the layout.

Ah Let's add the current fill and insert together:

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

Now we have a new problem. Windows Insets can be scheduled at any and multiple can be scheduled during the lifetime of the view. This means that our new logic will run well at the first run, but for each subsequent schedule, we will add more and more bottom fills. Not what we want. A kind of

The solution I came up with was to record the fill-in values of the view after inflation, and then refer to those values. 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
}

This works well, meaning that we keep filling in from the layout, and we still insert views as needed. Keeping the object-level attributes of each fill-in value is very confusing, and we can do better...

doOnApplyWindowInsets

Enter the doOnApply Windows Insets () extension method. This is setOnApplyWindowInsetsListener() A wrapper that outlines the above pattern:

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)

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 better! A kind of

requestApplyInsetsWhenAttached()

You may have noticed the request Apply Insets WhenAttached () above. This is not absolutely necessary, but it does address the way Windows Insets are dispatched. If the view is called when it is not attached to the view hierarchy requestApplyInsets() The call is placed on the floor and ignored.

This is in the Fragment.onCreateView() Common situations when creating views in. The fix is to ensure that the call is simple onStart() The method in, or using a listener after connection to request insets. The following extension functions deal with two situations:

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
        })
    }
}

Wrap it in a binding

At this point, we have greatly simplified how to handle window insertion. We actually use this feature in some upcoming applications, including upcoming conference apps. It still has some shortcomings. First, logic is far away from our layout, which means that it is easy to forget. Secondly, we may need to use it in many places, resulting in a large number of near-identical copies being disseminated 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're talking about views here, so in the ideal world we'll declare that we intend to process illustrations in layout files.

input data binding adapters ! If you've never used them before, they let us map our code to layout properties (when you use data binding). So let's create an attribute 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)
    }
}

In our layout, we can simply use our new padding Bottom System Windows Insets property, which will automatically update any inserts.

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

I hope you can see how it fits ergonomically and is easy to use compared to using OnApply Windows Listener alone. A kind of

But wait, bind adapter hard coding only sets the bottom size. What if we need to deal with top illustrations? Or the left? Or is it right? Fortunately, binding adapters allow us to well summarize patterns in 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
        )
    }
}

Here we have declared an adapter with multiple attributes, 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 set. This means that we can perform the following actions, such as setting the left and bottom sides:

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

Level of ease of use: (vi)

android: fitSystemWindows

You may have read this article and thought of Why hasn't he mentioned the fitSystem Windows attribute? The reason is that attributes often do not provide the functionality we want.

If you are using AppBarLayoutCoordinatorLayoutDrawerLayout With friends, use as directed. These views are built to identify attributes and to apply window insertion in a fixed manner associated with these views.

Android: The default View implementation for fitSystem Windows implies filling each dimension with insets, but it does not apply to the above example. For more information, see here blog post It's still very relevant.

Topics: Android Windows Attribute REST