6、Window 的 Type 类型与层级关系:FIRST_APPLICATION_WINDOW、FIRST_SUB_WINDOW、FIRST_SYSTEM_WINDOW 的层级范围与 Z-order 排序
Window Type,说白了就是给每个窗口发一个「身份证号」。这个号码决定了窗口在屏幕上的堆叠顺序——也就是 Z-order。我刚开始啃 WMS 源码时,就被这一堆 FIRST_XXX、LAST_XXX 搞得头晕。后来发现,其实 Android 把窗口分成了三大族群,每个族群有自己的「楼层范围」。
三大窗口族群
Android 定义了三类窗口,分别用三个宏来标记起始值:
- FIRST_APPLICATION_WINDOW —— 应用窗口,起始值 1
- FIRST_SUB_WINDOW —— 子窗口,起始值 1000
- FIRST_SYSTEM_WINDOW —— 系统窗口,起始值 2000
每个族群内部还有细分。比如系统窗口里,TYPE_STATUS_BAR 是 2000,TYPE_NAVIGATION_BAR 是 2019,TYPE_KEYGUARD 是 2004。你想想看,这些值其实就决定了谁压着谁。
核心规律:Type 值越大,窗口越靠近用户。系统窗口永远盖在应用窗口上面,子窗口依附于父窗口。
Z-order 排序规则
WMS 内部维护了一个 mWindowMap,每次添加窗口时,会根据 Type 值计算它的 Z-order。具体逻辑在 WindowManagerPolicy 里,我挑重点说:
- 先按族群分:应用窗口(1~99)在最底层,子窗口(1000~1999)在中间,系统窗口(2000~2999)在最顶层。
- 同族群内按 Type 值升序:比如
TYPE_STATUS_BAR(2000) 在TYPE_NAVIGATION_BAR(2019) 下面。 - 同 Type 内按添加顺序:后添加的窗口盖在先添加的上面。
嗯,这里要注意:子窗口的 Z-order 不是独立的。它跟着父窗口走。父窗口在哪个层级,子窗口就在那个层级附近浮动。
Type 值范围速查表
| 族群 | 起始宏 | Type 范围 | 典型窗口 |
|---|---|---|---|
| 应用窗口 | FIRST_APPLICATION_WINDOW | 1 ~ 99 | Activity 主窗口、Dialog 窗口 |
| 子窗口 | FIRST_SUB_WINDOW | 1000 ~ 1999 | PopupWindow、Spinner 下拉框 |
| 系统窗口 | FIRST_SYSTEM_WINDOW | 2000 ~ 2999 | 状态栏、导航栏、锁屏、Toast |
个人习惯:我写系统级弹窗时,喜欢用 TYPE_APPLICATION_OVERLAY (2038) 而不是 TYPE_SYSTEM_ALERT (2003)。因为前者在 Android 10+ 上权限要求更友好,而且不会莫名其妙被系统拦截。
层级关系示意图
下面这张图展示了三大族群在 Z 轴上的堆叠关系。我特意把系统窗口画在最上面,应用窗口在最下面,中间是子窗口——但子窗口其实跟着父窗口走,所以实际位置会动态变化。
避坑指南:Type 选择不当的后果
我曾经在一个项目中,为了做一个全局悬浮球,直接用了 TYPE_SYSTEM_ALERT。结果在 Android 8.0 以上的设备上,弹窗死活不显示。查了半天才发现,这个 Type 在 8.0 之后被限制了,必须用 TYPE_APPLICATION_OVERLAY 才行。
注意:不要随意使用系统窗口 Type。尤其是 TYPE_SYSTEM_OVERLAY (2006),它会在所有界面之上绘制,连状态栏和导航栏都盖得住。用不好会被 Google Play 下架。
源码中的 Type 定义
这些常量定义在 android.view.WindowManager.LayoutParams 里。我贴一段核心代码,你感受一下:
// 应用窗口范围
public static final int FIRST_APPLICATION_WINDOW = 1;
public static final int LAST_APPLICATION_WINDOW = 99;
// 子窗口范围
public static final int FIRST_SUB_WINDOW = 1000;
public static final int LAST_SUB_WINDOW = 1999;
// 系统窗口范围
public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int LAST_SYSTEM_WINDOW = 2999;
// 常用系统窗口 Type
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW; // 2000
public static final int TYPE_KEYGUARD = 2004;
public static final int TYPE_NAVIGATION_BAR = 2019;
public static final int TYPE_APPLICATION_OVERLAY = 2038;
你看,TYPE_STATUS_BAR 直接等于 FIRST_SYSTEM_WINDOW,也就是 2000。这意味着状态栏是系统窗口里「最底层」的那个。导航栏是 2019,比状态栏高 19 个层级。所以导航栏能盖在状态栏上面——嗯,这其实是个设计细节。
Z-order 计算逻辑
WMS 在 assignLayersLocked() 方法里计算每个窗口的 layer。核心逻辑是这样的:
- 应用窗口的 layer = type 值(1~99)
- 子窗口的 layer = 父窗口 layer + 1000 + 子窗口自己的 type 偏移
- 系统窗口的 layer = type 值(2000~2999)
说白了,系统窗口天生就比应用窗口大 1900 多。你应用窗口 Type 再大(最大 99),也超不过系统窗口的最小值 2000。这就是为什么系统弹窗永远盖在 Activity 上面。
我建议:如果你需要做一个「看起来像系统窗口」但又不想滥用系统权限,可以考虑用 TYPE_APPLICATION_OVERLAY + FLAG_NOT_FOCUSABLE。这样既能在应用上层显示,又不会抢焦点,用户体验会好很多。
实际项目中的经验
我记得有一次做车载系统,需要实现一个「紧急呼叫」按钮,要求在任何界面(包括锁屏)之上显示。我一开始用了 TYPE_KEYGUARD_DIALOG,结果发现它在某些定制 ROM 上被拦截了。后来换成 TYPE_SYSTEM_ERROR (2010) —— 这个 Type 的优先级非常高,几乎不会被系统过滤。但要注意,它会让手机震动并发出警告音,所以不能乱用。
嗯,总结一下:Type 选择不是随便填个数字就完事的。它关系到权限、兼容性、用户体验。我的原则是:能用应用窗口就别用系统窗口,能用 TYPE_APPLICATION_OVERLAY 就别用 TYPE_SYSTEM_ALERT。层级越高,责任越大。
公众号:蓝海资料掘金营,微信 deep3321