fitsSystemWindows到底是做什么的?

158

我不太理解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;
}

使用新的Material设计时,有一些新类会广泛使用该标志,这就是混淆产生的原因。在许多来源中,fitsSystemWindows被提及为设置将视图放置在系统栏后面的标志。请参见此处

ViewCompat.javasetFitsSystemWindows的文档说明如下:

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

据此,fitsSystemWindows只是意味着函数fitsSystemWindows()将被执行?新的Material类似乎只是将其用于在状态栏下方绘制。如果我们查看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并不能解决问题,因为默认实现会按照最初的文档进行操作。你必须覆盖它并添加与其他提到的类相同的标志。我错过了什么吗?


1
这似乎是一个错误,我已经在Android问题跟踪器上发布了一个错误报告 - Tim Rae
1
请查看此处:https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec?linkId=19685562#.9k2ax354g - Fatih S.
谢谢提供链接,非常有用。然而,它证实了那里存在不一致的问题。在链接页面中,它说一些新的小部件,例如CoordinatorLayout,使用该标志来推断它们是否应该在状态栏后面绘制。这在FrameLayout等小部件中并不适用。 - Pin
3
这是一个非常棒的问题,你在查看Android源代码时做得非常好。我特别欣赏你指出了新的MD类如何以不同的方式处理fitsSystemWindows... 我之前一直在试图弄清楚这个问题! - coolDude
3个回答

37

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

通常情况下,您的应用程序不需要在状态栏或导航栏下绘制,但如果需要:您需要确保交互元素(如按钮)不会被隐藏在它们下面。这就是 android:fitsSystemWindows=“true” 属性的默认行为:它设置视图的填充以确保内容不会覆盖系统窗口。

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


11
简而言之,如果你正在尝试确定是否使用fitsSystemWindows,那么Chris Banes(来自Android团队的开发人员)的Insetter库提供了一个更好的替代方案。有关更多详细信息,请参见下面的说明。
Android团队在2015年发表了一篇很好的文章 - 为什么我想要fitsSystemWindows?。它很好地解释了该属性的默认行为以及一些布局(如DrawerLayout)如何覆盖它。
但是,那是2015年的事了。在2017年的droidcon上,Chris Banes,一位从事Android工作的人,建议不要使用fitSystemWindows属性,除非容器文档说要使用它。原因是标志的默认行为通常无法满足您的期望。视频中有很好的解释。
但是,这些应该使用fitsSystemWindows的特殊布局是什么呢?嗯,它们是DrawerLayoutCoordinatorLayoutAppBarLayoutCollapsingToolbarLayout。这些布局覆盖了默认的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
) {
    // 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库,它提供了相同的功能,请参见insetter-dbx

还要注意,自androidx core库的1.5.0版本以来,WindowInsets API将会发生变化。例如,insets.systemWindowInsets变为insets.getInsets(Type.systemBars() or Type.ime())。有关更多详细信息,请参阅库文档和article

参考资料:


10

它不会在系统栏后面绘制,而是将其延伸到栏后并使用相同颜色对其进行着色,但它包含的视图会在状态栏内填充。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接