WindowInsets - 布局的监听器

若是您已经看过个人Becoming a Master Window Fitter谈话,您就会知道处理窗口插件可能很复杂。 最近,我一直在改进几个应用程序中的系统栏处理,使他们可以在状态和导航栏后面绘制。 我想我已经提出了一些方法,可使处理插入更容易(但愿如此)。原文html

在导航栏后面绘制

对于本文的其他部分,咱们将使用BottomNavigationView进行一个简单的示例,该示例位于屏幕底部。 它的实现很是简单:android

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent" />
复制代码

默认状况下,您的Activity的内容将在系统提供的UI(导航栏等)中进行布局,所以咱们的视图与导航栏齐平。 咱们的设计师决定他们但愿应用程序开始在导航栏后面绘制。 要作到这一点,咱们将使用适当的标志调用setSystemUiVisibility()swift

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
复制代码

最后咱们将更新咱们的主题,以便咱们有一个半透明的导航栏,带有黑色图标:windows

<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>
复制代码

如您所见,这只是咱们须要作的事情的开始。 因为活动如今正在导航栏后面,咱们的BottomNavigationView也是如此。 这意味着用户没法实际点击任何导航项。 为了解决这个问题,咱们须要处理系统调度的任何WindowInsets,并使用这些值对视图应用适当的填充或边距。app

Handling经过填充进行插入

处理WindowInsets的经常使用方法之一是为视图添加填充,以便它们的内容不会显示在system-ui后面。 为此,咱们能够设置OnApplyWindowInsetsListener,为视图添加必要的底部填充,确保其内容不被遮挡。ide

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

好的,咱们如今已经正确处理了底部系统窗口的插入。 但后来咱们决定在布局中添加一些填充,多是出于审美缘由:函数

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

嗯,那不对。 你能看到问题吗? 咱们从OnApplyWindowInsetsListener调用updatePadding()如今将从布局中消除预期的底部填充。post

啊哈! 让咱们一块儿添加当前填充和插入:ui

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

咱们如今有一个新问题。 WindowInsets能够在_any_时调度,_multiple_能够在视图的生命周期中调度。 这意味着咱们的新逻辑将在第一次运行时运行良好,可是对于每一个后续调度,咱们将添加愈来愈多的底部填充。 不是咱们想要的。🤦

我想出的解决方案是在通胀后记录视图的填充值,而后再参考这些值。 例:

// 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
}
复制代码

这很好用,意味着咱们从布局中保持填充的意图,咱们仍然根据须要插入视图。 保持每一个填充值的对象级属性是很是混乱的,咱们能够作得更好......🤔

doOnApplyWindowInsets

输入doOnApplyWindowInsets()扩展名方法。 这是[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) 复制代码

当咱们须要一个视图来处理insets时,咱们如今能够执行如下操做:

bottomNav.doOnApplyWindowInsets { view, insets, padding ->
    // padding contains the original padding values after inflation
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}
复制代码

好多了!😏

requestApplyInsetsWhenAttached()

您可能已经注意到上面的requestApplyInsetsWhenAttached()。 这不是绝对必要的,但确实能够解决WindowInsets的分派方式。 若是视图在未附加到视图层次结构时调用requestApplyInsets(),则会将调用放在地板上并忽略。

这是在[Fragment.onCreateView()](developer.android.com/reference/a… 修复方法是确保简单地调用[onStart()](developer.android.com/reference/a… 如下扩展函数处理两种状况:

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
        })
    }
}
复制代码

在绑定中包装它

在这一点上,咱们已经大大简化了如何处理窗口插入。 咱们实际上在一些即将推出的应用程序中使用此功能,包括即将举行的会议apps。 它仍然有一些缺点。 首先,逻辑远离咱们的布局,这意味着它很容易被遗忘。 其次,咱们可能须要在许多地方使用它,致使大量的near-identical副本在整个应用程序中传播。 我知道咱们能够作得更好。

到目前为止,整个帖子只关注代码,并经过设置监听器来处理insets。 咱们在这里讨论的是视图,因此在理想的世界中咱们会声明咱们打算在布局文件中处理插图。

输入data binding adapters! 若是您之前从未使用它们,它们会让咱们将代码映射到布局属性(当您使用数据绑定时)。 所以,让咱们为咱们建立一个属性:

@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)
    }
}
复制代码

在咱们的布局中,咱们能够简单地使用咱们新的paddingBottomSystemWindowInsets属性,该属性将自动更新任何插入。

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

但愿您可以看到与单独使用OnApplyWindowListener相比,它是如何符合人体工程学且易于使用的。🌠

但等等,绑定适配器硬编码只设置底部尺寸。 若是咱们还须要处理顶部插图怎么办? 仍是左边? 仍是对吗? 幸运的是,绑定适配器让咱们能够很好地归纳全部维度的模式:

@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
        )
    }
}
复制代码

这里咱们已经声明了一个具备多个属性的适配器,每一个属性都映射到相关的方法参数。 须要注意的一点是使用requireAll = false,这意味着适配器能够处理所设置属性的任意组合。 这意味着咱们能够执行如下操做,例如设置左侧和底部:

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

易用性等级:💯

android:fitSystemWindows

你可能已经阅读过这篇文章,并想到了_“Why hasn’t he mentioned the fitSystemWindows attribute?"_。 缘由是由于属性带来的功能一般不是咱们想要的。

若是您正在使用AppBarLayoutCoordinatorLayoutDrawerLayout和朋友,那么按照指示使用。 构建这些视图是为了识别属性,并以与这些视图相关的固定方式应用窗口插入。

android:fitSystemWindows的默认View实现意味着使用insets填充每一个维度,但不适用于上面的示例。 有关更多信息,请参阅此blog post,它仍然很是相关。

相关文章
相关标签/搜索