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里的一个排序算法。它综合考虑了三个因素:

  1. type值:基础层级,决定了窗口属于哪个“层组”
  2. subLayer:父子窗口中的子层偏移
  3. baseLayer:系统内部计算出的基准层

举个例子,TYPE_STATUS_BAR是2000,TYPE_NAVIGATION_BAR是2019。按数值看,导航栏应该盖在状态栏上面。但实际效果呢?状态栏在屏幕顶部,导航栏在底部,它们根本不在同一个区域叠放。

为什么会这样?因为WMS在计算Z-order时,还会考虑窗口的显示区域输入法交互。说白了,系统会尽量让同区域的窗口按合理顺序排列,而不是简单粗暴地按type值排序。

核心要点:TYPE_STATUS_BAR和TYPE_NAVIGATION_BAR的Z-order关系,不是简单的谁大谁小。它们各自在自己的显示区域内拥有最高优先级。当状态栏和导航栏在屏幕边缘相遇时(比如横屏模式),系统会通过额外的策略来调整叠放顺序。

SVG:SystemUI窗口层级结构图

SystemUI窗口层级结构 TYPE_SYSTEM_ERROR / TYPE_SYSTEM_ALERT (2010+) TYPE_NOTIFICATION_SHADE / TYPE_STATUS_BAR_PANEL (2024-2025) TYPE_NAVIGATION_BAR (2019) TYPE_STATUS_BAR (2000) TYPE_SYSTEM_OVERLAY (2006) 应用窗口层 (TYPE_APPLICATION, 1-99) 壁纸 / 桌面底层 系统错误/警告 通知面板 状态栏/导航栏 系统覆盖层

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