FitsSystemWindows 到底是做什么的?

我正在努力理解 fitsSystemWindows的概念,因为它根据不同的观点做不同的事情。根据官方文件

布尔内部属性,根据系统窗口(如状态栏)调整视图布局。如果为 true,则将此视图的填充调整为 为系统窗口留出空间

现在,检查 View.java类,我可以看到当设置为 true时,窗口插入(状态栏、导航栏...)应用于视图填充,根据上面引用的文档可以工作。这是守则的相关部分:

private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
Rect localInsets = sThreadLocal.get();
if (localInsets == null) {
localInsets = new Rect();
sThreadLocal.set(localInsets);
}
boolean res = computeFitSystemWindows(insets, localInsets);
mUserPaddingLeftInitial = localInsets.left;
mUserPaddingRightInitial = localInsets.right;
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}

随着新的材质设计有新的类,广泛使用这个标志,这就是混乱来了。在许多资源中,fitsSystemWindows被认为是设置为将视图放置在系统栏后面的标志。见 给你

setFitsSystemWindowsViewCompat.java文档说:

设置此视图是否应考虑系统屏幕修饰 如状态栏并插入其内容; < strong > 即控制是否 {@link View # fitSystemWindows (Rect)}的默认实现将为 有关更多细节,请参见该方法 .

根据这个,fitsSystemWindows只是意味着函数 fitsSystemWindows()将被执行?新的材质类似乎只是用来在状态栏下绘图。如果我们看看 DrawerLayout.java的代码,我们可以看到:

if (ViewCompat.getFitsSystemWindows(this)) {
IMPL.configureApplyInsets(this);
mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
}

...

public static void configureApplyInsets(View drawerLayout) {
if (drawerLayout instanceof DrawerLayoutImpl) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}

我们在新的 CoordinatorLayoutAppBarLayout中看到了相同的模式。

这是否与 fitsSystemWindows的文档以完全相反的方式工作? 在最后一种情况下,它的意思是 躲在系统栏后面

但是,如果您希望 FrameLayout在状态栏后面绘制自己,那么将 fitsSystemWindows设置为 true 并不能达到预期效果,因为默认实现会执行最初记录的操作。您必须重写它,并添加与其他提到的类相同的标志。我错过了什么吗?

69447 次浏览

它不会在系统栏后面绘制 它有点延伸后面的酒吧,以色彩它有相同的颜色,但它包含的意见是填充内的状态栏 如果可以的话

系统窗口是屏幕上系统正在绘制的部分 非交互式(在状态栏的情况下)或交互式 (在导航栏的情况下)内容。

大多数情况下,你的应用程序不需要在状态栏或 导航栏,但如果你这样做: 你需要确保互动 元素(比如按钮)不会隐藏在它们下面 Android 的默认行为: fitsSystemWindows = “ true”属性 给你: 它设置的填充视图,以确保内容 不要覆盖系统窗口。

Https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec

简而言之,如果你想弄清楚是否要使用 fitsSystemWindows,Chris Banes (Android 团队的一名开发人员)的 插图库提供了一个更好的 fitsSystemWindows替代品。有关更多细节,请参见下面的解释。

Android 团队在2015年发表了一篇很好的文章—— 我为什么要安装 SystemWindows?。它很好地解释了属性的默认行为,以及诸如 DrawerLayout 之类的布局如何覆盖它。

但那是2015年。早在2017年,在 Droidcon Chris Banes,谁在 Android 上工作,建议不使用 fitSystemWindows属性,除非容器文档说使用它。这样做的原因是,标志的默认行为往往不符合您的期望。视频里解释得很清楚。

但是什么是你应该使用 fitsSystemWindows特殊布局呢?是 DrawerLayout CoordinatorLayout AppBarLayoutCollapsingToolbarLayout。这些布局覆盖了默认的 fitsSystemWindows行为,并以一种特殊的方式对待它,这在视频中也有很好的解释。这种对属性的不同解释有时会导致混淆和像这样的问题。实际上,在 droidcon London 的 又一个视频中,Chris Banes 承认决定过载默认行为是一个错误(London conf 的时间戳是13:10)。

好的,如果 fitSystemWindows不是最终的解决方案,应该使用什么?在2019年的 另一篇文章中,Chris Banes 提出了另一种解决方案,一些基于 WindowInsets API的自定义布局属性。例如,如果您想要右下角的 FAB 从导航栏边距,您可以很容易地配置它:

<com.google.android.material.floatingactionbutton.FloatingActionButton
app:marginBottomSystemWindowInsets="@{true}"
app:marginRightSystemWindowInsets="@{true}"
... />

该解决方案使用定制的 @BindingAdapter,一个用于填充,另一个用于页边距。我在上面提到的 文章中已经很好地描述了这个逻辑。一些谷歌样本使用的解决方案,例如见 猫头鹰机器人材料应用程序,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
) {
// Create a snapshot of the view's padding & margin states
val initialPadding = recordInitialPaddingForView(this)
val initialMargin = recordInitialMarginForView(this)
// Set an actual OnApplyWindowInsetsListener which proxies to the given
// lambda, also passing in the original padding & margin states
setOnApplyWindowInsetsListener { v, insets ->
block(v, insets, initialPadding, initialMargin)
// Always return the insets, so that children can also use them
insets
}
// request some 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) {
// 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 : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}


override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}

正如你所看到的,领悟并不是微不足道的。正如我之前提到的,欢迎您使用 Chris Banes 的 插图库,它提供了相同的功能,请参见 Insetter-dbx

还要注意,自从 Androidx 核心库的1.5.0版本以来,WindowInsetsAPI 将会发生变化。例如,insets.systemWindowInsets变成 insets.getInsets(Type.systemBars() or Type.ime())。有关更多细节,请参见库文档和 文章

参考文献: