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 四大核心模块依赖关系 StatusBar 状态栏 NavigationBar 导航栏 NotificationPanel 通知面板 QuickSettings 快速设置 SystemUI Core 事件总线 / 依赖注入 / 生命周期 说明:实线箭头表示主要控制流和事件通知;虚线表示面板内容共享(通知面板包含 QuickSettings 区域) 所有模块通过 SystemUI Core 进行依赖注入和事件通信,不直接引用对方

模块间的依赖约定

从图里能看出来,每个模块都不直接知道其他模块的细节,而是通过 SystemUI Core 这个中介来通信。具体依赖关系我整理成了一张表:

模块A依赖B依赖方式常见场景
StatusBarNotificationPanel通过 CommandQueue 发送展开/收起指令用户从状态栏下拉时,StatusBar 通知 Panel 展开
StatusBarNavigationBar通过 BatteryController 等共享状态电量改变时,状态栏和导航栏都要更新图标
NotificationPanelQuickSettingsPanel 内部持有 QS 的 Fragment 引用展开面板时,QS 区域同步展开/折叠
NavigationBarKeyguard(锁屏)通过 StatusBarKeyguardViewManager锁屏时禁用导航栏手势
所有模块SystemUI Core依赖注入(Dagger 或手动绑定)获取生命周期、事件总线、系统服务代理

注意循环依赖:曾经我在 NotificationPanel 里直接引用了 StatusBar 的实例,导致单元测试出现循环依赖,一跑测就 StackOverflow。正确的做法是把公共状态抽离到 StatusBarStateController 这样的共享接口里。

避坑指南:模块拆分常见的三个误操作

做了这么多年 SystemUI 定制,我总结了几条拆模块时容易踩的坑——说白了都是血泪教训。

  1. 别让 NotificationPanel 直接操作 StatusBar 的绘制。我在一个项目里为了做“展开通知面板时状态栏背景变暗”的效果,直接在 Panel 里改了 StatusBar 的 View 属性。结果锁屏解锁时,状态栏背景一直暗着回不去。正确做法是监听面板状态,通过回调让 StatusBar 自己更新。
  2. NavigationBar 的键值不要写死。有些 OEM 厂商会把返回键放到左边,Home 键放右边。如果你在 NavigationBar 里硬编码按键坐标,换一个设备就崩。要始终通过 NavigationBarInflater 读取布局资源。
  3. QuickSettings 的 Tile 更新不要直接使用 Context 启动 Activity。Tile 的点击最终需要 QSHost 帮你代理启动,否则在锁屏下会触发安全异常。我之前因为偷懒直接传 Context.startActivity,被 CTS 测试打回来了三次。

小结:模块划分的核心原则

回过头来看,这四大模块的划分其实遵循了一个很朴素的思路:它们各自只关心自己那一亩三分地。StatusBar 只管显示图标和接收点击;NavigationBar 只管按键和手势;NotificationPanel 只管通知内容;QuickSettings 只管快速开关。边界清晰了,依赖就不容易乱。

你想想看,如果哪天产品经理说“要把导航栏上的返回键换成一个小工具箱”,你只需要改 NavigationBar 一个模块,其他三个模块完全不用动。这就是模块划分带来的实际好处。我个人觉得,SystemUI 的架构设计值得每个 Android 开发者仔细观摩——它不只是代码的堆砌,更像一套精密的生态协议。