SystemSystemWindows究竟做了什么?

Pin*_*Pin 113 android android-layout

我正在努力理解这个概念,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;
}
Run Code Online (Sandbox Code Playgroud)

随着新材料设计的出现,有一些新的类广泛使用这个标志,这就是混乱的来源.在许多来源中fitsSystemWindows被提到作为设置在系统栏后面放置视图的标志.看到这里.

在文档ViewCompat.javasetFitsSystemWindows说:

设置此视图是否应考虑系统屏幕装饰,例如状态栏和插入其内容; 也就是说,控制是否执行{@link View#fitSystemWindows(Rect)}的默认实现.有关详细信息,请参阅该方法.

据此,fitsSystemWindows仅仅意味着该功能fitsSystemWindows()将被执行?新的Material类似乎只是用于在状态栏下绘图.如果我们查看DrawerLayout.java代码,我们可以看到:

if (ViewCompat.getFitsSystemWindows(this)) {
        IMPL.configureApplyInsets(this);
        mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
    }
Run Code Online (Sandbox Code Playgroud)

...

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们在新的CoordinatorLayout或者看到相同的模式AppBarLayout.

这不是与文档完全相反的方式fitsSystemWindows吗?在最后一种情况下,它意味着在系统栏后面绘制.

但是,如果您希望FrameLayout在状态栏后面绘制自己,则设置fitsSystemWindows为true不会起到作用,因为默认实现会执行最初记录的内容.您必须覆盖它并添加与其他提到的类相同的标志.我错过了什么吗?

ilj*_*yya 14

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

大多数情况下,您的应用程序不需要在状态栏或导航栏下绘制,但如果您这样做:您需要确保交互式元素(如按钮)不会隐藏在它们下面.这就是android:fitsSystemWindows ="true"属性的默认行为为您提供:它设置View的填充以确保内容不会覆盖系统窗口.

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


Hal*_*a.M 5

它不会在系统栏后面绘制,而是在系统栏后面延伸以使用相同的颜色对其进行着色,但是如果有意义的话,将其包含的视图填充在状态栏中


Val*_*kov 5

简而言之,如果您想弄清楚是否要使用fitsSystemWindows,可以使用Chris Banes(来自 Android 团队的开发人员)的Insetter库,它提供了一个更好的替代fitsSystemWindows. 有关更多详细信息,让我们看看下面的解释。

Android 团队在 2015 年发表了一篇很好的文章 -为什么我要 fitsSystemWindows?. 它很好地解释了属性的默认行为以及像 DrawerLayout 这样的布局如何覆盖它。

但是,那是 2015 年。早在 2017 年的 droidcon上,在 Android 上工作的Chris Banes建议不要使用fitSystemWindows属性,除非容器文档说要使用它。这样做的原因是标志的默认行为通常不符合您的期望。这在视频中得到了很好的解释。

但是,您应该在哪些地方使用这些特殊布局fitsSystemWindows?嗯,它是DrawerLayoutCoordinatorLayoutAppBarLayoutCollapsingToolbarLayout。这些布局会覆盖默认fitsSystemWindows行为并以特殊方式对其进行处理,这在视频中再次得到了很好的解释。对属性的这种不同解释有时会导致混淆和问题,例如这里。实际上,在droidcon London 的另一个视频中,Chris Banes 承认重载默认行为的决定是一个错误(伦敦会议的 13:10 时间戳)。

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

<com.google.android.material.floatingactionbutton.FloatingActionButton
  app:marginBottomSystemWindowInsets="@{true}"
  app:marginRightSystemWindowInsets="@{true}"
  ... />
Run Code Online (Sandbox Code Playgroud)

该解决方案使用 custom @BindingAdapters,一个用于填充,另一个用于边距。这个逻辑在我上面提到的文章中有很好的描述。一些谷歌示例使用该解决方案,例如参见Owl android material app, 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
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,实现并非微不足道。正如我之前提到的,欢迎您使用Chris Banes 提供的Insetter库,它提供相同的功能,请参阅insetter-dbx

另请注意,自androidx 核心库1.5.0 版以来,WindowInsets API 将发生变化。例如insets.systemWindowInsets变成insets.getInsets(Type.systemBars() or Type.ime()). 有关更多详细信息,请参阅库文档和文章

参考: