终于翻译完了 由于我并不是很懂英语 并且里面的部分名词不知道如何翻译 就采用了机翻或者直接删掉、改为我理解中的意思 所以本文章内容可能和原文有出入
如有侵权请联系!
如果您观看了我 Becoming a Master Window Fitter 的演讲,你就会知道处理窗口插入视图(以下统一使用机翻:插图)可能很复杂。 最近,我一直在改善一些应用程序中的系统栏处理功能,使它们可以隐藏状态栏和导航栏。 我想出了一些方法,可以更轻松地处理插图。
在导航栏后面绘图
对于本文的其余部分,我们将使用 BottomNavigationView
演示一个简单的示例,该示例位于屏幕底部。 它非常简单地实现为:
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent" />
默认情况下,您活动的内容将布置在系统提供的UI(导航栏等)中,因此我们的视图与导航栏齐平。 我们的设计师已决定,他们希望该应用开始在导航栏后面绘制。 为此,我们将使用适当的标记调用 setSystemUiVisibility()
:
rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
注:以上代码使用的是Kotlin 想看Java版的可以看看我之前转载的文章:关于Android 10的手势导航条适配问题
最后,我们将更新主题,以使我们拥有一个带有深色图标的半透明导航栏:
<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 ,并使用这些值将适当的填充应用于视图。
通过填充处理插图
处理 WindowInsets 的常用方法之一是向视图中添加填充,以使它们的内容不显示在系统视图的后面。 为此,我们可以设置一个 OnApplyWindowInsetsListener 来向视图添加必要的底部填充,以确保其内容不会被遮盖。
bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
好的,我们现在已经正确处理了系统底部的插图。 但是后来,设计师决定在底部的布局中也添加一些填充:
<BottomNavigationView
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingVertical="24dp" />
作者注:我不建议在 BottomNavigationView 上使用 24dp的垂直填充,此处使用较大的值只是为了使效果更加明显。
嗯,那是不对的。 你看到问题了吗? 现在,我们从 OnApplyWindowInsetsListener
中对 updatePadding()
的调用将清除布局中预期的底部填充:
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
// 设置一个实际的 OnApplyWindowInsetsListener 代理
// lambda, also passing in the original padding state
// lambda,也以原始填充状态传递
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)
啊哈! 让我们将当前的填充和插图添加在一起:
bottomNav.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(
bottom = view.paddingBottom + insets.systemWindowInsetsBottom
)
insets
}
我们现在有一个新问题。 WindowInsets 可以在视图的生命周期中的 任何 时间分配,并且可以 多次 分配。 这意味着我们的新逻辑将在第一次的调用运行良好,但是对于每次调用,我们将添加越来越多的底部填充。 不是我们想要的。🤦
我想出的解决方案是在第一次调用后记录视图的填充值,然后再引用这些值。 例:
// 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()
的包装,它概括了上面的模式。
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)
现在,当我们需要一个视图来处理插图时,我们可以执行以下操作:
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()
中创建视图时,这是常见的情况。 解决方法是确保仅在 onStart()
中调用该方法,或者使用附加的监听器请求插入。 以下扩展功能可处理两种情况:
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
})
}
}
将其包装
至此,我们已经大大简化了处理窗口插图的方法。😉但它仍然有一些缺点:
- 逻辑与我们的布局背道而驰,这意味着很容易忘记。
- 我们可能需要在许多地方使用它,导致大量几乎完全相同的代码散布在整个应用程序中。
我知道我们可以做得更好。
到目前为止,整个文章都只专注于代码,并通过设置监听器来处理插图。 不过,我们这里只是在讨论视图,因此在理想情况下,我们将声明要处理布局文件中的插图的方法。
使用 data binding adapters ! 如果您以前从未使用过它们,则可以让我们将代码映射到布局属性(使用数据绑定时)。 您可以在这里阅读有关它们的更多信息:
Make Data Binding Do What You Want(至少10个人需要就立刻翻译)
因此,让我们创建一个属性来为我们执行此操作:
@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
您可能已经阅读了这篇文章,并且问“他为什么没有提到fitSystemWindows属性?”这样做的原因是因为属性带来的功能通常不是我们想要的。
如果您使用的是 AppBarLayout, CoordinatorLayout, DrawerLayout 等,那么可以使用它(注:在我使用后同样达不到效果 不知道是什么原因)。 这些视图已构建为可以识别属性,并以与这些视图相关的自觉方式应用窗口插图。
android:fitSystemWindows
的默认 View 实现意味着可以使用插图填充每个尺寸,并且不适用于上面的示例。 有关更多信息,请参阅此博客文章,该文章仍然非常相关:
作者的话
啊,这是一篇超长的文章! 除了让我们更轻松地处理 WindowInsets
之外,还希望它展示了扩展功能,lambda和绑定适配器等功能如何使任何API易于使用。
感谢尼克·切尔(Nick Butcher)和扎拉·多明格斯(Zarah Dominguez)。