若是您已经看过个人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
处理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()
扩展名方法。 这是[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()
。 这不是绝对必要的,但确实能够解决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 }" />
复制代码
易用性等级:💯
你可能已经阅读过这篇文章,并想到了_“Why hasn’t he mentioned the fitSystemWindows attribute?"_。 缘由是由于属性带来的功能一般不是咱们想要的。
若是您正在使用AppBarLayout,CoordinatorLayout,DrawerLayout和朋友,那么按照指示使用。 构建这些视图是为了识别属性,并以与这些视图相关的固定方式应用窗口插入。
android:fitSystemWindows
的默认View实现意味着使用insets填充每一个维度,但不适用于上面的示例。 有关更多信息,请参阅此blog post,它仍然很是相关。