重温 fitsSystemWindows

感觉这个是特别令人困惑的属性,不仅我,网上也有很多人也类似感受。时间一久我常常容易忘记 fitsSystemWindows 的具体作用,所以今天复习一遍。

官方文档 Hide the status bar | Android Developers 中提到:

Android 4.1 开始,可以将 app 的内容绘制在状态栏的下一层 (behind the status bar),并且 app 的内容区域的大小不会随着状态栏的出现和消失而自动。使用 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 即可实现这种效果。
将 app 的内容绘制在状态栏的下一层时,app 自己负责保证 UI 中的重要内容不被状态栏遮挡(例如,地图应用中的控件不应被状态档遮挡)。否则可能导致应用不可用。大部分时候通过在 XML 文件中添加 android:fitsSystemWindows=true 可以解决这个被遮挡导致不可用的问题。这个属性用于告知父节点要为 system windows 保留一些 padding。

关于 system window:

System windows are the parts of the screen where the system is drawing either non-interactive (in the case of the status bar) or interactive (in the case of the navigation bar) content.

文档中的内容并不难理解:这个属性用于告知父节点要为 system windows 保留一些 padding。通过截图说明一下使用 fitsSystemWindows=true 和使用 fitsSystemWindows=false 的区别。

显然第一张图是我们期望的效果。如果真实情况这么简单,就没有什么让人困惑的了。下面来看问题。


补充知识

setFitsSystemWindows 方法是跟 fitsSystemWindows 属性对应的方法。这个方法的作用描述如下:

Sets whether or not this view should account for system screen decorations such as the status bar and inset its content; that is, controlling whether the default implementation of fitSystemWindows(android.graphics.Rect) will be executed.


问题

测试机是华为 Nova 2 (Android 8.0),Android 8.0。

Activity 使用的主题中将 windowTranslucentStatus 设置如 true。内容如下:

1
2
3
4
5
6
7
<resources>

<style name="AppTheme" parent="BaseAppTheme">
<item name="android:windowTranslucentStatus">true</item>
</style>

</resources>

如果 Activity 布局文件的根布局是 DrawerLayoutfitsSystemWindows 表现正常。

1
2
3
4
5
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

但如果 Activity 布局文件的根布局是 LinearLayoutfitsSystemWindows 表现就很诡异了。

1
2
3
4
5
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">

怎么个诡异法呢。如果 fitsSystemWindows=false,效果如下,跟预期一致:

如果 fitsSystemWindows=true,效果如下,状态栏变成白色的了:

  • 为什么 DrawerLayout 中使用 fitsSystemWindows=true 是正常,而 LinearLayout 中使用 fitsSystemWindows=true 不正常
  • 为什么 fitsSystemWindows=true 似乎跟 windowTranslucentStatus=true 有冲突

android - What exactly does fitsSystemWindows do? - Stack Overflow 这个帖子为上面第一个问题提供了一些线索。Material 包中的很多类,比如 DrawerLayoutCoordinatorLayoutAppBarLayout等等,并不是仅仅将 fitsSystemWindows 用于控制是否让父节点要为 system windows 保留一些 padding,还用来控制是否修改状态栏背景!

比如,DrawerLayout.java 源码中可以看到如下用法:

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

可以推测,LinearLayout 并没有类似处理,所以看起来状态栏变白了。可以断定第一个问题的原因正是这种对 fitsSystemWindows 处理上的不一致性。也正是这种不一致性导致 fitsSystemWindows 让人困惑。令人困惑的fitsSystemWindows属性 - 简书 也提到这种不一致问题,称这种不一致为个性化(似乎也有一定道理,官方并没有说只能用一种固定的方式处理 fitsSystemWindows)。

解决办法

知道了问题原因,解决办法就简单了。

  • 从主题中去掉 windowTranslucentStatus=true
  • 或者在使用 LinearLayout 等时主动设置状态栏颜色

Why would I want to fitsSystemWindows? – Android Developers – Medium 这篇文章讲得不错。翻译一下。

首先说说什么是 system window。system window 即 Android 系统本身进行绘制的区域,既可以是无交互的(状态栏),也可以是有交互的(导航栏)。

大部分时候你的应用不需要在状态栏或导航栏的底部进行绘制,但如果一定要这样做的时候,你应该保证应用中的可交互元素不会被状态栏或导航栏遮挡。这就是 android:fitsSystemWindows=true 的缺省行为:它让 View 有足够的 padding, 保证内容不会被 system window 遮拦。

记住这几点:

  • fitsSystemWindows 是深度优先的。顺序很重要,it’s the first View that consumes the insets that makes a difference
  • Insets are always relative to the full window - 布局前就添加了 inset,所以在为父节点添加 padding 时其实并不知道 view 的位置
  • padding 会被 fitsSystemWindows=true 覆盖 - 同一个 view 上使用 fitsSystemWindows=true 后 padding 相关的属性无效

以上所说的对多数应用场景是足够的,比如视频播放。

如果想让 RecyclerView 在透明的导航栏底部滚动,使用 android:fitsSystemWindows="true"android:clipToPadding="false" 即可。

关于如何自定义 fitsSystemWindows。

在 KitKat 及以下,自定义 View 可以覆盖 fitsSystemWindows() 方法。如果消费了 inset,让 fitsSystemWindows() 返回 true,否则就返回 false

在 Lollipop 及以上版本,添加了一些新的 API 以便更容易实现 fitsSystemWindows(),并且跟 View 的其他行为保持一致。你可以覆盖 onApplyWindowInsets() 方法,这个方法允许 View 消费掉指定大小的 inset,也可以根据需要让子节点调用 dispatchApplyWindowInsets()

更方便的是,甚至可以不用继承 View 就能在 Lollipop 及以上版本中实现自定义的行为,只需使用 ViewCompat.setOnApplyWindowInsetsListener()。这个方法优先于 View.onApplyWindowInsets() 来调用。ViewCompat 还提供辅助方法用于调用 onApplyWindowInsets()dispatchApplyWindowInsets()

自定义 fitsSystemWindows 示例

FrameLayoutLinearLayout 等基本布局实现了缺省的 fitsSystemWindows 行为,另外一些布局则为特定的使用场景实现了自定义 fitsSystemWindows 行为。

一个例子是 navigation drawer

上图中的 DrawerLayout 使用了 fitsSystemWindows=true。这个属性让 DrawerLayout 了子节点有 inset,但仍然会按照 material design 规范绘制状态栏的背景(缺省使用colorPrimaryDark)。

Lollipop 及以上版本中,DrawerLayout 为每个子节点调用 dispatchApplyWindowInsets(),以允许每个子节点也可以有 fitsSystemWindows,这跟缺省的行为有所不同(缺省行为中父节点会消费 fitsSystemWindows,子节点收不到 fitsSystemWindows)。

CoordinatorLayout also takes advantage of overriding how it handles window insets, allowing the Behavior set on child Views to intercept and change how Views react to window insets, before calling dispatchApplyWindowInsets() on each child themselves. It also uses the fitsSystemWindows flag to know if it needs to paint the status bar background.

Similarly, CollapsingToolbarLayout looks for fitsSystemWindows to determine when and where to draw the content scrim — a full-bleed scrim which overlays the status bar area when the CollapsingToolbarLayout is scrolled sufficiently off the screen.

If you’re interested in seeing some of the common cases that accompany the Design Library, check out the cheesesquare sample app

Use the system, don’t fight it
One thing to keep in mind is that it isn’t called fitsStatusBar or fitsNavigationBar. What constitutes system windows, their dimensions, and location may certainly change with different platform releases — for a perfect example, look at the differences between Honeycomb and Ice Cream Sandwich.

Just rest assured that the insets you do get from fitsSystemWindows will be correct on all platform versions to ensure your content does not overlap with system provided UI components — make sure to avoid any assumptions on their availability or size if you customize the behavior.


参考