SystemUI 模块划分:四大核心模块的职责
模块划分这件事,说白了就是为了让代码好维护、好扩展。我之前在接手一个定制化项目时,发现整个 SystemUI 所有逻辑都塞在一个大文件里,改个通知图标颜色都能引发导航栏闪退。那次之后,我彻底理解了「高内聚、低耦合」不是空话。
这一章咱们就掰扯清楚 SystemUI 最核心的四个模块——StatusBar、NavigationBar、NotificationPanel、QuickSettings。它们各管一摊,但又彼此依赖。搞懂这些,后面分析依赖关系时才不会懵。
StatusBar(状态栏)
状态栏,就是屏幕最顶上那一小条。嗯,它的职责其实比大多数人想象的要重。
- 系统状态显示:信号强度、Wi-Fi、蓝牙、电量、时间等图标
- 通知图标聚合:只显示通知的图标,不显示内容
- 系统图标管理:闹钟、静音、VPN、录屏等特殊状态图标
- 交互入口:点击时间进入日历,点击电池进入电量详情,下拉触发通知面板
关键点:StatusBar 本身并不渲染通知内容,它只是通知的“缩略图标展示器”。真正的通知内容由 NotificationPanel 负责。我见过有人把通知气泡渲染逻辑塞进 StatusBar,结果导致下拉动画卡顿——这是典型的职责不清。
NavigationBar(导航栏)
导航栏就是屏幕底部的三颗键(或手势区域)。手势模式下它变成一条细线,但代码层面依然是同一个模块。
| 职责 | 说明 |
|---|---|
| 按键/手势处理 | 返回、Home、最近任务 |
| 沉浸模式管理 | 根据 Activity 的 SYSTEM_UI_FLAG 隐藏/显示导航栏 |
| 手势动画协调 | 配合 WindowManager 处理底部上滑、侧滑返回等手势 |
| 边缘防误触 | 游戏或全屏视频时限制手势区域灵敏度 |
我个人习惯把 NavigationBar 看作一个「系统手势调度中心」。它不仅要响应点击,还要和 Launcher、当前 App、甚至输入法争夺手势控制权。有一次我调全屏沉浸模式,发现返回手势死活不生效,最后查了三天才发现是 NavigationBar 的监听注册顺序和 Keyguard 的触控事件冲突了……嗯,这种坑以后有机会细聊。
NotificationPanel(通知面板)
从屏幕顶部往下拉——整个覆盖区域就是 NotificationPanel。它承载了两套东西:通知列表和快速设置(QuickSettings)。但在架构上,它们是两个独立模块。
- 通知堆叠:按应用分组,支持展开、折叠、滑动删除
- 通知优先级:静音通知折叠,重要通知置顶
- 通知历史:提供查看最近删除通知的入口
- 面板动画:联动下拉手势、展开/收起过渡
小技巧:如果你自己写通知面板的展开动画,记得用 ViewPropertyAnimator 配合插值器,别直接用 ValueAnimator 硬算位置。之前项目里我同事用后者,每次下拉都有肉眼可见的抖动,后来改成前者就好了。
QuickSettings(快速设置)
它就是通知面板顶部那一排小按钮——Wi-Fi、蓝牙、手电筒、旋转开关等等。看起来简单,但背后涉及动态 Tile 注册、状态同步、甚至第三方 Tile 的权限管理。
| 核心职责 | 技术要点 |
|---|---|
| Tile 管理 | 每个 Tile 是一个 QSTile 实例,通过 QSHost 注册 |
| 状态监听 | 监听蓝牙、Wi-Fi 等系统服务状态变化并实时更新图标 |
| 拖拽排序 | 允许用户自定义 Tile 顺序并持久化到 SettingsProvider |
| 面板展开/折叠 | 快速设置区域本身也有两级展开状态 |
这里我踩过一个坑:第三方 App 注册的 Tile,在快速设置里点击后跳转到宿主 App,但用户如果没安装那个 App,点击后直接崩溃。后来我加了一层 PackageManager.resolveActivity 校验——说白了,永远别信第三方 Tile 的 Intent 是合法的。
模块间的关系图谱
光看每个模块的职责还不够,你得明白它们怎么互相调用。我画了一张图,帮你把它们的关系理清楚。
模块间的依赖约定
从图里能看出来,每个模块都不直接知道其他模块的细节,而是通过 SystemUI Core 这个中介来通信。具体依赖关系我整理成了一张表:
| 模块A | 依赖B | 依赖方式 | 常见场景 |
|---|---|---|---|
| StatusBar | NotificationPanel | 通过 CommandQueue 发送展开/收起指令 | 用户从状态栏下拉时,StatusBar 通知 Panel 展开 |
| StatusBar | NavigationBar | 通过 BatteryController 等共享状态 | 电量改变时,状态栏和导航栏都要更新图标 |
| NotificationPanel | QuickSettings | Panel 内部持有 QS 的 Fragment 引用 | 展开面板时,QS 区域同步展开/折叠 |
| NavigationBar | Keyguard(锁屏) | 通过 StatusBarKeyguardViewManager | 锁屏时禁用导航栏手势 |
| 所有模块 | SystemUI Core | 依赖注入(Dagger 或手动绑定) | 获取生命周期、事件总线、系统服务代理 |
注意循环依赖:曾经我在 NotificationPanel 里直接引用了 StatusBar 的实例,导致单元测试出现循环依赖,一跑测就 StackOverflow。正确的做法是把公共状态抽离到 StatusBarStateController 这样的共享接口里。
避坑指南:模块拆分常见的三个误操作
做了这么多年 SystemUI 定制,我总结了几条拆模块时容易踩的坑——说白了都是血泪教训。
- 别让 NotificationPanel 直接操作 StatusBar 的绘制。我在一个项目里为了做“展开通知面板时状态栏背景变暗”的效果,直接在 Panel 里改了 StatusBar 的 View 属性。结果锁屏解锁时,状态栏背景一直暗着回不去。正确做法是监听面板状态,通过回调让 StatusBar 自己更新。
- NavigationBar 的键值不要写死。有些 OEM 厂商会把返回键放到左边,Home 键放右边。如果你在 NavigationBar 里硬编码按键坐标,换一个设备就崩。要始终通过
NavigationBarInflater读取布局资源。 - QuickSettings 的 Tile 更新不要直接使用
Context启动 Activity。Tile 的点击最终需要QSHost帮你代理启动,否则在锁屏下会触发安全异常。我之前因为偷懒直接传 Context.startActivity,被 CTS 测试打回来了三次。
小结:模块划分的核心原则
回过头来看,这四大模块的划分其实遵循了一个很朴素的思路:它们各自只关心自己那一亩三分地。StatusBar 只管显示图标和接收点击;NavigationBar 只管按键和手势;NotificationPanel 只管通知内容;QuickSettings 只管快速开关。边界清晰了,依赖就不容易乱。
你想想看,如果哪天产品经理说“要把导航栏上的返回键换成一个小工具箱”,你只需要改 NavigationBar 一个模块,其他三个模块完全不用动。这就是模块划分带来的实际好处。我个人觉得,SystemUI 的架构设计值得每个 Android 开发者仔细观摩——它不只是代码的堆砌,更像一套精密的生态协议。