19、SystemUI的窗口层级:WindowManager.LayoutParams的类型与层级,TYPE_STATUS_BAR与TYPE_NAVIGATION_BAR的Z-order
说到SystemUI的窗口层级,这其实是个挺有意思的话题。我刚开始接触Android框架层时,总觉得窗口层级就是个简单的数字大小问题——数字大的盖住数字小的。后来在项目中踩过几次坑才发现,事情远没那么简单。
嗯,咱们今天就把这块彻底聊透。
WindowManager.LayoutParams的类型体系
先说说WindowManager.LayoutParams里的type字段。这个字段决定了窗口的“身份”——说白了,就是告诉WindowManagerService:我是什么类型的窗口,我该待在哪一层。
Android系统里定义了二十多种窗口类型,但咱们SystemUI主要关注这么几类:
| 类型常量 | 数值 | 用途 |
|---|---|---|
| TYPE_STATUS_BAR | 2000 | 状态栏窗口 |
| TYPE_NAVIGATION_BAR | 2019 | 导航栏窗口 |
| TYPE_NOTIFICATION_SHADE | 2024 | 通知 shade 面板 |
| TYPE_STATUS_BAR_PANEL | 2025 | 状态栏下拉面板 |
| TYPE_SYSTEM_OVERLAY | 2006 | 系统级覆盖层 |
| TYPE_SYSTEM_ERROR | 2010 | 系统错误提示 |
你可能会问:为什么这些数字是2000、2019这样的?其实这是Android系统预留的区间。应用层的窗口类型在1到1999之间,系统窗口从2000开始往上排。我个人习惯把这个区间叫做“系统保留地”——普通应用碰不到,只有系统进程才能用。
Z-order的真相:不只是数字大小
很多人以为Z-order就是type数值越大越靠上。这个理解其实只对了一半。
真正决定窗口叠放顺序的,是WindowManagerService里的一个排序算法。它综合考虑了三个因素:
- type值:基础层级,决定了窗口属于哪个“层组”
- subLayer:父子窗口中的子层偏移
- baseLayer:系统内部计算出的基准层
举个例子,TYPE_STATUS_BAR是2000,TYPE_NAVIGATION_BAR是2019。按数值看,导航栏应该盖在状态栏上面。但实际效果呢?状态栏在屏幕顶部,导航栏在底部,它们根本不在同一个区域叠放。
为什么会这样?因为WMS在计算Z-order时,还会考虑窗口的显示区域和输入法交互。说白了,系统会尽量让同区域的窗口按合理顺序排列,而不是简单粗暴地按type值排序。
核心要点:TYPE_STATUS_BAR和TYPE_NAVIGATION_BAR的Z-order关系,不是简单的谁大谁小。它们各自在自己的显示区域内拥有最高优先级。当状态栏和导航栏在屏幕边缘相遇时(比如横屏模式),系统会通过额外的策略来调整叠放顺序。
SVG:SystemUI窗口层级结构图
TYPE_STATUS_BAR与TYPE_NAVIGATION_BAR的Z-order细节
咱们重点看看这两个类型。我记得在Android 10之前,状态栏和导航栏的Z-order关系其实挺简单的——导航栏的type值大,所以它在上层。但后来Google发现一个问题:当用户在全屏模式下,导航栏需要临时隐藏,而状态栏可能还在显示通知图标。这时候如果导航栏盖在状态栏上面,就会出现视觉上的撕裂感。
所以从Android 10开始,WMS引入了一个叫mPolicy的策略接口。这个接口里有一个方法:
// WindowManagerPolicy.java
int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow);
// 内部实现大致逻辑
switch (type) {
case TYPE_STATUS_BAR:
return 21; // 状态栏层
case TYPE_NAVIGATION_BAR:
return 22; // 导航栏层(略高)
case TYPE_NOTIFICATION_SHADE:
return 23; // 通知 shade 更高
}
你看,虽然type值相差19,但实际映射到的内部层只差1。这意味着导航栏确实比状态栏高一层,但差距非常小。为什么要这样设计?
我个人理解是:导航栏需要处理触摸事件,如果它和状态栏在同一层,当用户从底部上滑时,事件可能会被状态栏拦截。高出一层可以确保导航栏的触摸区域优先响应。
小技巧:如果你在开发SystemUI相关功能时,需要让某个窗口盖在状态栏或导航栏上面,不要直接用TYPE_SYSTEM_OVERLAY。我建议使用TYPE_STATUS_BAR_PANEL或TYPE_SYSTEM_ERROR,它们的层级更可控,而且不会和输入法、锁屏等系统组件产生冲突。
避坑指南:我曾经踩过的坑
我曾经在一个项目中,需要实现一个自定义的系统级悬浮球。当时图省事,直接用了TYPE_SYSTEM_OVERLAY。结果在Android 11上测试时发现:当用户打开通知 shade 时,悬浮球竟然盖在了通知列表上面!
后来排查发现,TYPE_SYSTEM_OVERLAY的Z-order在Android 11中被调整了。它从原来的2006变成了一个动态计算的值,在某些场景下会高于通知面板。解决方案是改用TYPE_SYSTEM_ERROR(2010),这个类型的层级一直很稳定,而且系统对它有一些额外的保护机制,不容易被其他窗口覆盖。
嗯,这里要注意:TYPE_SYSTEM_ERROR虽然名字叫“错误”,但它完全可以用来显示正常的UI。系统只是用它来确保某些关键UI不会被用户误操作遮挡。
Z-order的动态调整机制
你以为Z-order在窗口创建时就固定了?其实不是。WMS在运行时会根据多种因素动态调整窗口的叠放顺序:
- 焦点变化:获得焦点的窗口会被提升到同类型窗口的最上层
- 动画状态:正在播放动画的窗口会暂时获得更高的Z-order
- 输入法状态:输入法弹出时,相关窗口的层级会重新计算
- 多窗口模式:分屏或自由窗口模式下,层级策略完全不同
举个例子,当你在看视频时点击搜索框,输入法弹出来。这时候WMS会临时提升输入法窗口的Z-order,同时把状态栏和导航栏的层级稍微压低一点,确保输入法能正常显示。等输入法关闭后,层级又恢复原样。
这种动态调整机制,说白了就是系统在“和稀泥”——尽量让所有窗口都能正常工作,又不会互相遮挡得太难看。
总结一下
TYPE_STATUS_BAR和TYPE_NAVIGATION_BAR的Z-order关系,可以这么记:
- 数值上:导航栏(2019) > 状态栏(2000)
- 实际层级:导航栏比状态栏高1层(内部层21 vs 22)
- 显示区域:各自在屏幕顶部和底部,互不干扰
- 动态调整:WMS会根据焦点、动画、输入法等场景动态微调
你想想看,如果哪天你发现状态栏被导航栏盖住了,或者导航栏不见了,大概率是某个系统窗口的type值设置错了,或者WMS的层级计算出了bug。这时候别慌,先看看logcat里有没有关于WindowState的警告信息,再检查一下你的窗口type是否在系统保留区间内。
好了,关于SystemUI的窗口层级,咱们就聊到这儿。记住一点:窗口层级不是死数字,而是一套动态的策略。理解了这一点,你就能更好地掌控SystemUI的显示行为。
公众号:蓝海资料掘金营,微信deep3321