什么是 Insets
屏幕上除了开发者 app 绘制的内容还有系统的 Insets(插入物),Insets 区域负责描述屏幕的哪些部分会与系统 UI 相交。如 Starus bar 或 Navigation bar:
常见的 Insets 有:
STATUS_BAR,用于展示系统时间,电量,wifi 等信息NAVIGATION_BAR,虚拟导航栏(区别于实体的三大金刚键),形态有三大金刚键导航,手势导航两种。(有些设备形态如 TV 没有导航栏)IME,软键盘,用于输入文字
其中 STATUS_BAR 与 NAVIGATION_BAR 又被称为 System bar。
如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置。
在源码中,Insets 对象拥有 4 个 int 值,用于描述矩形四个边的偏移:
📢 注意:不要把 Insets 的
top,bottom,left,right与 Rect 的搞混,前者描述的是偏移,后者是坐标。
关于 Insets 更详尽的信息,可以 查看这篇文章。
setSystemUiVisibility 与 WTFs
View 的源码中有一个 setSystemUiVisibility() 的方法,虽然该方法在 Android 11 已被弃用,但按照本专栏的一贯风格,我们还是要来介绍一下该方法。
有些场景开发者可能希望 app 的内容可以绘制到状态栏或导航栏的区域以提供更好的用户体验,因此系统提供了 setSystemUiVisibility 方法,开发者可以通过向该方法传入不同的 flag 以应对不同的使用场景。
这些 flag 被称为 Window Transform Flags,简称 WTFs(滑稽脸😏),同样的,它们在 Android 11 中被弃用。常用的 flag 如下:
SYSTEM_UI_FLAG_VISIBLE
SYSTEM_UI_FLAG_LOW_PROFILE
SYSTEM_UI_FLAG_HIDEN_NAVIGATION
SYSTEM_UI_FLAG_FULLSCREEN
SYSTEM_UI_FLAG_IMMERSIVE_STICKY
SYSTEM_UI_FLAG_IMMERSIVE
SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
SYSTEM_UI_FLAG_LAYOUT_STABLE
SYSTEM_UI_FLAG_LAYOUT_HIDEN_NAVIGATION
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
如果想了解这些 flag 的效果可以参考 # Android setSystemUiVisibility详解。
System bar 能力变化史
Android 4.4 之前
用户内容显示在 System bar 之间,即下图红框所在区域:
开发者可以使用 setSystemUiVisibility 方法将内容绘制到状态栏后面,下图红框区域:
Android 4.4
Android 4.4 引入了 android:windowTranslucentStatus 和 android:windowTranslucentNavigation ,允许开发者将 System bar 设置成透明:
System barbackground 是由 WindowManager 绘制的(利用 Window 的 flag)
Android 5.0
之前版本 System bar 都是由 WindowManager 绘制的,在 Android 5.0,引入了 android:windowDrawsSystemBarBackgrounds,当 windowDrawsSystemBarBackgrounds 为 true(默认值) 时,System bar 的 background 在 Window 内部。如下图:
开发者可以调用 Window 的方法为 System bar 设置颜色:

📢注意:
windowTranslucentStatus和windowTranslucentNavigation要比为System bar设置自定义颜色的优先级更高。当
windowTranslucentStatus或windowTranslucentNavigation设置为 true 后会导致windowDrawsSystemBarBackgrounds为 false,System barbackground 由 WindowManager 接管。
自 Android 5.0 后,当 windowDrawsSystemBarBackgrounds 为 true 时,System bar 作为 window 的一部分。换言之,DecorView(FrameLayout 子类)有三个子 View:显示 App 内容的 LinearLayout 以及 Status bar 和 Navigation bar。
默认情况下,App 的内容显示在 System bar 中间。
理论上,显示 App 内容的 LinearLayout 应该充满屏幕,系统使用了 paddingTop 和 marginBottom 为 System bar 预留出了空间。
那么 App 的内容区域是如何绘制到 System bar 后面的?很简单,LinearLayout 没有 padding 和 margin(我们在后文介绍原理),充满屏幕:
Android 10
随着全面屏设备的普及,越来越多的 Android 设备突破了 16:9 的限制,Android 10 推出了新的导航模式:手势导航。
新的手势导航与原来的三大金刚键的 Navigation bar 一样,只不过高度变小了。
如果 Navigation bar 是透明的,底部的「小白条」是可以跟随背景动态改变颜色的(与 iOS 一样,不知道谁抄的谁🤣)
Android 11
Android 11 引入了 WindowInsetsAnimation 允许监听 Insets 的变化进度,使用户体验更加丝滑。
小结
为了方便开发者更合理地使用设备屏幕绘制内容,Android 在历代版本不断迭代 System bar 控制的 API,功能越来越完善。
当开发者将 App 内容绘制到 System bar 后面时要考虑视觉冲突和手势冲突。
为了防止 App 内容区域与 System bar 发生视觉冲突,官方提供了两种 API, WidowInsets 与 fitsSystemWindows 。
WindowInsets
WindowInsets 描述了一组 Window Content 的 Insets,未来可能会继续添加新的 Insets 类型。目前已有的 Insets 类型有:
package androidx.core.view;
public class WindowInsetsCompat {
public static final class Type {
static final int FIRST = 1;
static final int STATUS_BARS = FIRST;
static final int NAVIGATION_BARS = 1 << 1;
static final int CAPTION_BAR = 1 << 2;
static final int IME = 1 << 3;
static final int SYSTEM_GESTURES = 1 << 4;
static final int MANDATORY_SYSTEM_GESTURES = 1 << 5;
static final int TAPPABLE_ELEMENT = 1 << 6;
static final int DISPLAY_CUTOUT = 1 << 7;
static final int LAST = 1 << 8;
static final int SIZE = 9;
static final int WINDOW_DECOR = LAST;
private Type() {}
}
}
使用位运算管理状态是很常见并高效的方式,如果对这部分内容不了解,可以移步 KunMinX 的 这篇文章 就算不去火星种土豆,也请务必掌握的 Android 状态管理最佳实践! 入门
System bar 包括 Status bar,Navigation bar,Caption bar,但不包括软键盘(ime)
onApplyWindowInsets 与 setOnApplyWindowInsetsListener
开发者可以通过在自定义 View 中重写 onApplyWindowInsets() 方法或调用 setOnApplyWindowInsetsListener() 来监听 WindowInsets 的变化,通过对 View 添加 margin 或 padding 的方式处理解决冲突。
这两个方法是互斥的,当存在 OnApplyWindowInsetsListener 时不会执行 onApplyWindowInsets:
开发者可以在也可
OnApplyWindowInsetsListener手动调用onApplyWindowInsets使两个方法同时被执行。
WindowInsets 分发
前文我们提到,如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置,此时 View#onApplyWindowInsets() 会被调用。那么这些 Insets 是如何分发给 View 的呢?
笔者在 View 事件分发机制,大型职场 PUA 现场 一文中把 Android 的视图树抽象为 N 叉树。
与 View 的事件分发一样,WindowInsets 的分发也是 N 叉树的遍历过程:
从 N 叉树的根节点(DecoView)开始,按照 深度优先 的方式分发给 子 view。
Android 10 和 Android 11 两个版本官方连续修改了 ViewGroup#dispatchApplyWindowInsets() 的逻辑(具体我们在源码解析篇介绍)。
如果 app targetSdkVersion < 30 ,如果某个节点消费了 Insets,所有没遍历到的节点都不会收到 WindowInsets 的分发;
当 app 运行在 Android 11 以上版本的设备上且 targetSdkVersion >= 30,如果某个节点消费了 Insets,该节点的所有子节点不会收到 WindowInsets 分发。
旧版本的分发有一个问题,无法做到两个同级的 View 同时消费 WindowInsets,如下图:
我们可以将 Level2-1 和 Level2-2 看成顶部导航和底部导航,按照旧逻辑,当 Level2-1 消费了
WindowInsets,另一个 View 便没机会了。
小结
- 由于开发者可以将 App 内容绘制到与系统 UI 相交的位置,因此官方为开发者提供了解决视觉冲突的方式,
WindowInsets - 开发者可以重写
View#onApplyWindowInsets或View#setOnApplyWindowInsetsListener来根据 WindowInsets 对系统 UI 进行位置避让(对 view 设置 padding 或 margin)。 - 下一节介绍的
fitsSystemWindows的默认行为也是通过onApplyWindowInsets实现的。
fitsSystemWindows

setFitsSystemWindows 是 View 中 API 14 后加入的方法,对应的 xml 属性是 android:fitsSystemWindows
fitsSystemWindows 的默认行为是:通过 padding 为 System bar 预留出空间。如前文提到的 DecorView 的 LinearLayout,它的 paddingTop 就是 fitsSystemWindows = true 影响的。
默认情况下 DecorView 的子 view 是 inflate
screen_simple.xml得到的。
那么这个 padding 是如何设置的?
View#onApplyWindowInsets() 中会判断 fitsSystemWindows 最终调用到 internalSetPadding() 方法:
📢 注意:这会使开发者在 xml 中定义的 padding 失效。
fitsSystemWindows 这个 API 另很多开发者迷惑,一个重要原因是很多时候 fitsSystemWindows 并不是使用的默认行为,如 DrawerLayout 和 CoordinatorLayout。
DrawerLayout
DrawerLayout fitsSystemWindow = true 时:
- API > 21 时设置
setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) -
onMeasure()时调用子 viewdispatchApplyWindowInsets()(正常父 View 消费WindowInsets后子 View 接收不到分发) -
onDraw()时调用setStatusBackground(?android:colorPrimaryDark)
CoordinatorLayout
CoordinatorLayout fitsSystemWindow = true 时:
- API > 21 时设置
setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) - 根据需要
setStatusBackground - 允许设置 behavior 的子 View 拦截并响应
WindowInsets的变化
小结
关于 fitsSystemWindows,你必须知道以下几点:
fitsSystemWindows是深度优先(我们可以将视图树看成一个 N叉树)的,第一个设置fitsSystemWindows的 view 会去消费 insets 并影响视觉;- padding 在 view layout 之前就已经设置了,因此不要误认为设置padding 时了解 view 所在的位置
- 开发者在 xml 或 view 初始化设置的 padding 会被覆盖
- AppBarLayout,CoordinatorLayout,DrawerLayout 等 view 会自定义
fitsSystemWindows的行为
处理 WindowInsets 的最佳实践
使用 Jetpack 提供的 Compat API
Android Jetpack 组件库中的 androidx.core 提供了大量兼容旧版本的 Compat API,如 ViewCompat,WindowInsetsCompat,WindowInsetsControllerCompat 等等。
下图是 ViewCompat#getWindowInsetsController 方法,用于获取 WindowInsetsController,同时兼容低版本:
获取WindowInsets

使用 ViewCompat.getRootWindowInsets(view) 获取 WindowInsets。请注意:
- 该方法返回分发给视图树的原始 insets
- insets 只有在 view attached 才是可用的
- API 20 及以下 永远 返回 false
获取 System bar 和 软键盘的高度
❌ 错误用法
🙅🏻♀️不要固定 status bar 的高度
res/values
<dimen name="status_bar_size">25dp</dimen>
res/values-v23
<dimen name="status_bar_size">24dp</dimen>
不同 Android 版本 status bar 的高度是不同的!不同设备也可能定制自己的高度。
🙅🏻♀️读取系统内部资源
framework 的 dimens.xml 存储了一些列系统内部资源。
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId)
如果系统内部资源名称变化怎么办?
「野路子」代码可能有效,但不健壮。
✅ 正确用法
- 获取
WindowInsets - 通过
WindowInsets#getInsets(type)获取 Insets - 通过 Insets.top 或 Insets.bottom 获取
System bar高度
为了兼容旧版本,我们使用 Compat API:
-
val windowInsetsCompat = ViewCompat.getRootWindowInsets(view)获取 WindowInsets -
val insets = windowInsetsCompat?.getInsets(WindowInsetsCompat.Type.statusBars())获取 Insets -
insets?.top或insets?.bottom获取System bar高度
ViewCompat.getRootWindowInsets(window.decorView)?.getInsets(WindowInsetsCompat.Type.statusBars())?.top
ViewCompat.getRootWindowInsets(window.decorView)?.getInsets(WindowInsetsCompat.Type.statusBars())?.bottom
当 System bar 隐藏时 getInsets() 获取的高度为 0,如果想在隐藏状态时也能获取高度,可以使用 getInsetsIgnoringVisibility() 方法
ViewCompat.getRootWindowInsets(window.decorView)?.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.statusBars())?.top
ViewCompat.getRootWindowInsets(window.decorView)?.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.statusBars())?.bottom
WindowInsetsController
Android 30 引入了 WindowInsetsController 来控制 WindowInsets,主要功能包括:
- 显示/隐藏
System bar - 设置
System bar前景(如状态栏的文字图标)是亮色还是暗色 - 逐帧控制 insets 动画,例如可以让软键盘弹出得更丝滑
显示隐藏 System bar
// 状态栏是否可见
ViewCompat.getRootWindowInsets(view)?.isVisible(WindowInsetsCompat.Type.statusBars()) ?: true
// 显示状态栏
ViewCompat.getWindowInsetsController(view).show(WindowInsetsCompat.Type.statusBars())
// 隐藏状态栏
ViewCompat.getWindowInsetsController(view).hide(WindowInsetsCompat.Type.statusBars())
// 导航栏是否可见
ViewCompat.getRootWindowInsets(view)?.isVisible(WindowInsetsCompat.Type.navigationBars()) ?: true
// 显示导航栏
ViewCompat.getWindowInsetsController(view).show(WindowInsetsCompat.Type.navigationBars())
// 隐藏导航栏
ViewCompat.getWindowInsetsController(view).hide(WindowInsetsCompat.Type.navigationBars())
// 软键盘是否可见
ViewCompat.getRootWindowInsets(view)?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
// 显示软键盘
ViewCompat.getWindowInsetsController(view).show(WindowInsetsCompat.Type.ime())
// 隐藏软键盘
ViewCompat.getWindowInsetsController(view).hide(WindowInsetsCompat.Type.ime())
设置 System bar 前景亮色/暗色
ViewCompat.getWindowInsetsController(view).isAppearanceLightStatusBars = isLight
ViewCompat.getWindowInsetsController(view).isAppearanceLightNavigationBars = isLight
适配 edge-to-edge
何为 edge-to-edge?如下图,即应用内容的绘制范围从顶部状态栏下方开始,延伸至底部导航栏上方:
关于 edge-to-edge 的适配,官方文档 写得很完整,主要分三步:
// 1. 使内容区域全屏
WindowCompat.setDecorFitsSystemWindows(window, false)
// 2. 设置 System bar 透明
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
// 3. 可能出现视觉冲突的 view 处理 insets
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// 此处更改的 margin,也可设置 padding,视情况而定
view.updateLayoutParams<MarginLayoutParams> {
topMargin = insets.top
leftMargin = insets.left
bottomMargin = insets.bottom
rightMargin = insets.right
}
WindowInsetsCompat.CONSUMED
}
注意:处理 insets 时要保证计算操作有幂等性,即多次进行该计算所得到的结果应该相同,否则 margin/padding 会越来越大!
处理 insets 也可以通过 重写
View#onApplyWindowInsets来操作。
总结
- 随着 Android 的不断迭代,开发者可以更充分地利用屏幕空间,能够将内容绘制在系统 UI 后面;
- Android 使用 Insets 来描述系统 UI 与屏幕相交的区域,开发者可以使用
fitsSystemWindows和WindowInsets来处理视觉和手势冲突; -
WindowInsets的分发根据targetSDKVersion的不同而略有差别; -
fitsSystemWindows的默认行为是:通过 padding 为System bar预留出空间,本质也是利用 WindowInsets 处理视觉冲突; - 一些自定义 view 如 DrawerLayout 会更改
fitsSystemWindows的默认行为; - 处理
WindowInsets可以使用 Jetpackandroidx.core提供的一些列 Compat 类; - 牢记获取
Status bar高度的正确姿势,并避免错误用法; - 适配 edge-to-edge 以给用户更好的使用体验
推荐阅读和参考资源
- 开启全面屏体验 | 手势导航 (一)
- 处理视觉冲突 | 手势导航 (二)
- 如何处理手势冲突 | 手势导航连载 (三)
- 沉浸模式 | 手势导航连载 (四)
- Why would I want to fitsSystemWindows?
- WindowInsets — listeners to layouts
- Becoming a master window fitter🔧