简而言之,如果你正在尝试确定是否使用
fitsSystemWindows
,那么Chris Banes(来自Android团队的开发人员)的
Insetter库提供了一个更好的替代方案。有关更多详细信息,请参见下面的说明。
Android团队在2015年发表了一篇很好的文章 -
为什么我想要fitsSystemWindows?。它很好地解释了该属性的默认行为以及一些布局(如DrawerLayout)如何覆盖它。
但是,那是2015年的事了。在2017年的droidcon上,
Chris Banes,一位从事Android工作的人,
建议不要使用
fitSystemWindows
属性,除非容器文档说要使用它。原因是标志的默认行为通常无法满足您的期望。视频中有很好的解释。
但是,这些应该使用
fitsSystemWindows
的特殊布局是什么呢?嗯,它们是
DrawerLayout
、
CoordinatorLayout
、
AppBarLayout
和
CollapsingToolbarLayout
。这些布局覆盖了默认的
fitsSystemWindows
行为,并以特殊方式处理它,视频中有很好的解释。这种属性的不同解释有时会导致混淆和问题,就像这里提出的问题一样。实际上,在droidcon伦敦的
另一个视频中,Chris Banes承认过载默认行为的决定是一个错误(伦敦会议的13:10时间戳)。
好的,如果
fitSystemWindows
不是终极解决方案,那么应该使用什么呢?在2019年的
另一篇文章中,Chris Banes提出了另一种解决方案,基于
WindowInsets API的几个自定义布局属性。例如,如果你想让一个位于右下角的FAB与导航栏保持一定的距离,你可以轻松地进行配置:
<com.google.android.material.floatingactionbutton.FloatingActionButton
app:marginBottomSystemWindowInsets="@{true}"
app:marginRightSystemWindowInsets="@{true}"
... />
该解决方案使用自定义的
@BindingAdapter
,一个用于填充,另一个用于边距。逻辑在我上面提到的
article中有很好的描述。一些Google示例使用了这个解决方案,例如看看
Owl android材料应用程序,
BindingAdapters.kt。我在这里只是复制适配器代码供参考:
@BindingAdapter(
"paddingLeftSystemWindowInsets",
"paddingTopSystemWindowInsets",
"paddingRightSystemWindowInsets",
"paddingBottomSystemWindowInsets",
requireAll = false
)
fun View.applySystemWindowInsetsPadding(
previousApplyLeft: Boolean,
previousApplyTop: Boolean,
previousApplyRight: Boolean,
previousApplyBottom: Boolean,
applyLeft: Boolean,
applyTop: Boolean,
applyRight: Boolean,
applyBottom: Boolean
) {
if (previousApplyLeft == applyLeft &&
previousApplyTop == applyTop &&
previousApplyRight == applyRight &&
previousApplyBottom == applyBottom
) {
return
}
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
)
}
}
@BindingAdapter(
"marginLeftSystemWindowInsets",
"marginTopSystemWindowInsets",
"marginRightSystemWindowInsets",
"marginBottomSystemWindowInsets",
requireAll = false
)
fun View.applySystemWindowInsetsMargin(
previousApplyLeft: Boolean,
previousApplyTop: Boolean,
previousApplyRight: Boolean,
previousApplyBottom: Boolean,
applyLeft: Boolean,
applyTop: Boolean,
applyRight: Boolean,
applyBottom: Boolean
) {
if (previousApplyLeft == applyLeft &&
previousApplyTop == applyTop &&
previousApplyRight == applyRight &&
previousApplyBottom == applyBottom
) {
return
}
doOnApplyWindowInsets { view, insets, _, margin ->
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.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = margin.left + left
topMargin = margin.top + top
rightMargin = margin.right + right
bottomMargin = margin.bottom + bottom
}
}
}
fun View.doOnApplyWindowInsets(
block: (View, WindowInsets, InitialPadding, InitialMargin) -> Unit
) {
val initialPadding = recordInitialPaddingForView(this)
val initialMargin = recordInitialMarginForView(this)
setOnApplyWindowInsetsListener { v, insets ->
block(v, insets, initialPadding, initialMargin)
insets
}
requestApplyInsetsWhenAttached()
}
class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int)
class InitialMargin(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
)
private fun recordInitialMarginForView(view: View): InitialMargin {
val lp = view.layoutParams as? ViewGroup.MarginLayoutParams
?: throw IllegalArgumentException("Invalid view layout params")
return InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
}
fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
requestApplyInsets()
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
正如您所见,这个实现并不简单。如我之前提到的,您可以使用Chris Banes的Insetter库,它提供了相同的功能,请参见insetter-dbx。
还要注意,自androidx core库的1.5.0版本以来,WindowInsets API将会发生变化。例如,insets.systemWindowInsets
变为insets.getInsets(Type.systemBars() or Type.ime())
。有关更多详细信息,请参阅库文档和article。
参考资料:
CoordinatorLayout
,使用该标志来推断它们是否应该在状态栏后面绘制。这在FrameLayout
等小部件中并不适用。 - Pin