Android 14 的 SystemUI 是整套系统里最“显眼”的模块,用户每天点亮屏幕第一眼看到的就是它,下拉一下、划一下、点一下,全是 SystemUI 在响应。也正因为如此,它成了定制需求最集中的地方:锁屏要改、状态栏要改、快速设置面板要改,改完之后还得保证不崩、不卡、不闪。我最近刚在一个 RK3576 的 Android 14 板子上完整走了一遍 SystemUI 定制,从锁屏布局到状态栏图标,再到 QS 面板的磁贴重排,中间踩了不少坑,也积累了一些比较实用的经验。这篇文章就把整个改造过程拆开来讲,重点放在“为什么这么改”和“改完怎么验证”上,而不是只丢一堆代码。适合有一定 Android 系统开发基础、正在做 ROM 定制或设备定制的朋友参考,新手也能跟着思路理解 SystemUI 的整体结构。
1. 先搞清楚 Android 14 SystemUI 的模块边界
动手改之前,如果连 SystemUI 里哪个东西归哪个包管都没弄清楚,后面基本就是盲人摸象。Android 14 的 SystemUI 相比早期版本,模块化程度更高了,很多功能被拆成了独立的库和模块,不再是以前那种一个大包全塞进去的结构。
1.1 SystemUI 的核心组成与代码分布
在 AOSP 源码里,SystemUI 主要位于frameworks/base/packages/SystemUI。但到了 Android 14,你会发现很多实际逻辑已经不在这个目录下了,而是被挪到了frameworks/base/packages/SystemUI/shared以及一些独立的 module 里。比如锁屏相关的Keyguard逻辑,核心部分在com.android.systemui.keyguard包下,但锁屏的界面渲染又和NotificationShade、StatusBar有千丝万缕的联系。
我一般会把 SystemUI 分成这么几块来看:
- 状态栏(StatusBar):顶部那条,显示时间、电量、信号、通知图标。核心类包括
StatusBar、PhoneStatusBarView、CollapsedStatusBarFragment。 - 通知栏与快速设置(NotificationShade / QS):下拉出来的那一大块,上半部分是 QS 磁贴,下半部分是通知列表。核心类有
NotificationShadeWindowView、NotificationPanelViewController、QSPanelController。 - 锁屏(Keyguard):包括锁屏界面、密码输入、指纹/人脸提示等。核心类有
KeyguardViewMediator、KeyguardSecurityContainer、KeyguardClockSwitch。 - 导航栏(NavigationBar):底部三大金刚或手势条。Android 14 上手势导航是主流,但很多定制设备仍然需要三键导航。
这里有个容易混淆的点:状态栏和锁屏并不是完全独立的。锁屏界面上的状态栏其实是同一个 StatusBar 实例,只是处于不同的状态(KEYGUARD状态)。所以改状态栏的时候,一定要同时看锁屏下的表现,否则很容易出现“解锁后正常,锁屏上错位”的问题。
1.2 定制前必须确认的编译与调试环境
在 RK3576 这类板子上做定制,和纯 AOSP 模拟器不太一样。RK3576 是瑞芯微的芯片,Android 14 的 BSP 通常由芯片原厂或方案商提供,SystemUI 的代码可能已经被改过一轮了。所以第一步不是急着改代码,而是先确认你手上的代码基线。
我一般会做这几件事:
- 确认 SystemUI 的编译产物路径:在
out/target/product/<device>/system/system_ext/priv-app/SystemUI/下找到SystemUI.apk,确认它是不是从你修改的源码编译出来的。 - 确认 overlay 机制:Android 14 上很多资源是通过 RRO(Runtime Resource Overlay)覆盖的,先检查
device/<vendor>/<device>/overlay/下有没有已经存在的 SystemUI overlay,避免你改了半天被 overlay 覆盖掉。 - 打开调试开关:在
Settings > System > Developer options里打开“显示布局边界”和“GPU 渲染模式分析”,这两个对后面调布局非常有用。
提示:RK3576 的 Android 14 BSP 里,SystemUI 有时会被预编译成 odex 格式,直接 push APK 可能不生效。建议在源码里改完后整编
make SystemUI,然后重新烧录 system 分区,或者至少确保关闭了 dex2oat 的预编译优化。
1.3 为什么不能直接改 APK 而要改源码
有些人图省事,直接反编译 SystemUI.apk 改 smali 或者资源,然后重新打包。这种做法在小改动上偶尔能成,但在 Android 14 上风险极高。原因有几个:一是 Android 14 对系统应用的签名校验更严格,重新打包后签名不一致可能导致 SystemUI 无法启动;二是 SystemUI 和 framework 之间有大量隐藏 API 调用,反编译后很难保证兼容;三是 odex/vdex 的存在让直接替换 APK 变得不可靠。
所以我的建议很明确:只要你能拿到源码,就老老实实改源码重新编译。反编译只适合做逆向分析,不适合做正式定制。
2. 锁屏改造:从布局到交互的完整链路
锁屏是用户接触最多的界面之一,定制需求也最五花八门。有人要改时钟样式,有人要加自定义快捷方式,有人要去掉某个提示。Android 14 的锁屏结构比之前更复杂,因为引入了更多的可配置项和新的时钟布局。
2.1 锁屏界面的视图层级拆解
要改锁屏,先得知道锁屏界面是怎么拼出来的。Android 14 的锁屏主要视图层级大致是这样的:
KeyguardViewMediator负责整体调度,决定什么时候显示锁屏、什么时候隐藏。KeyguardSecurityContainer是锁屏的安全区域容器,里面装着密码/图案/指纹的输入界面。KeyguardClockSwitch负责时钟区域的切换,Android 14 支持多种时钟样式(数字、模拟、自定义)。NotificationShadeWindowView是整个下拉窗口的根视图,锁屏和通知栏共用这个根视图。
这里有个关键点:锁屏的时钟区域和通知区域是分开管理的。时钟在KeyguardClockSwitch里,通知在NotificationStackScrollLayout里。如果你想改时钟位置,改的是KeyguardClockSwitch的布局参数;如果你想改通知在锁屏上的显示方式,改的是NotificationStackScrollLayout在锁屏状态下的配置。
我实际改的时候,遇到过一个典型问题:把时钟往下移之后,通知列表和时钟重叠了。原因是时钟区域的父容器高度没有同步调整,导致通知列表的起始位置还是按原来的算。解决办法是同时修改KeyguardClockSwitch的layout_height和NotificationStackScrollLayout的topMargin,让两者保持联动。
2.2 自定义锁屏时钟样式的实操步骤
Android 14 原生支持几种时钟样式,但如果你想加一个完全自定义的样式,比如带天气信息的时钟,就需要自己扩展。下面是我实际操作的步骤:
- 在
KeyguardClockSwitch的布局文件里增加自定义时钟的 ViewStub。找到keyguard_clock_switch.xml,在里面加一个ViewStub,指向你自己的布局文件。 - 创建自定义时钟的 Controller。在
com.android.systemui.keyguard包下新建一个类,继承KeyguardClockController或者实现相应的接口,负责更新时钟显示。 - 在
KeyguardClockSwitchController里注册你的 Controller。根据当前配置决定是否加载自定义时钟。 - 处理配置变更。锁屏时钟在横竖屏切换、语言切换时都需要重新布局,确保你的自定义 View 能正确响应
onConfigurationChanged。
这里有个坑:Android 14 的锁屏时钟样式是可以通过系统设置切换的,如果你加的自定义样式没有在设置里注册,用户切换样式时你的时钟就会消失。所以要么在设置里加一个入口,要么把你的样式设为默认并禁用其他样式。
2.3 锁屏快捷方式的添加与移除
锁屏底部的快捷方式(默认是电话和相机)也是定制高频点。Android 14 上,这两个快捷方式由KeyguardBottomAreaView管理。要改的话,主要涉及两个地方:
- 快捷方式的图标和点击事件:在
KeyguardBottomAreaView里找到mLeftIconView和mRightIconView,替换它们的图标资源和OnClickListener。 - 快捷方式的可用性判断:有些设备没有相机,或者电话功能被禁用,这时候需要动态隐藏对应的快捷方式。Android 14 里通过
KeyguardBottomAreaView.updateBottomArea()来判断,你可以在这里加自己的逻辑。
我踩过的一个坑是:直接替换图标资源后,图标大小不对。原因是KeyguardBottomAreaView对图标有固定的尺寸约束,你换的图标如果原始尺寸差太多,就会被拉伸或压缩。解决办法是在 drawable 里用inset或者scaleType做适配,而不是直接换一张图。
2.4 锁屏与 AOD(息屏显示)的联动注意事项
很多定制设备会同时改锁屏和 AOD。Android 14 上,AOD 和锁屏共享一部分视图,但又有独立的显示逻辑。如果你改了锁屏时钟,AOD 上的时钟可能不会自动跟着变,因为 AOD 走的是DozingClockView而不是KeyguardClockSwitch。
所以改锁屏时钟的时候,一定要同步检查 AOD 的时钟显示。我一般的做法是:把时钟的绘制逻辑抽成一个独立的工具类,锁屏和 AOD 都调用这个工具类,这样改一处就能两边生效。虽然前期多花点时间,但后期维护省事很多。
3. 状态栏定制:图标、布局与交互的细节处理
状态栏看起来简单,就是顶部一条,但里面的细节非常多。Android 14 的状态栏在布局上更加灵活,支持动态隐藏、图标分组、通知图标数量限制等。定制的时候,稍不注意就会出现图标重叠、间距不对、锁屏下错位等问题。
3.1 状态栏布局文件的关键节点
状态栏的布局主要在status_bar.xml和phone_status_bar.xml里。Android 14 上,状态栏的根布局是PhoneStatusBarView,里面包含三个主要区域:
- 左侧区域:通常显示通知图标、运营商信息。
- 中间区域:显示时钟。
- 右侧区域:显示系统图标(信号、WiFi、电量等)。
这三个区域在布局上是用LinearLayout或者ConstraintLayout组织的。Android 14 默认用的是ConstraintLayout,因为要支持不同屏幕尺寸和刘海屏的适配。
改布局的时候,我建议先搞清楚每个区域的id,然后在CollapsedStatusBarFragment里找到对应的findViewById和初始化逻辑。比如你想把时钟从中间移到左边,改布局文件只是第一步,还得改CollapsedStatusBarFragment里对时钟位置的判断逻辑,否则代码里还是会按中间位置来算。
3.2 系统图标显示与隐藏的逻辑控制
状态栏右侧的系统图标(信号、WiFi、电量、蓝牙等)是由StatusBarIconController统一管理的。Android 14 上,图标的显示和隐藏通过StatusBarIconController.addIcon()和removeIcon()来控制。
如果你想定制某个图标的显示条件,比如“只在 WiFi 连接时显示 WiFi 图标,否则隐藏”,需要找到对应的IconManager或者StatusBarSignalPolicy。以 WiFi 为例,逻辑在StatusBarSignalPolicy里,它会根据WifiManager的回调来更新图标状态。
这里有个实际经验:不要直接在布局里把某个图标 View 设为GONE。因为状态栏的图标是动态添加和移除的,你在布局里隐藏了,代码里添加的时候还是会把它显示出来。正确的做法是修改StatusBarIconController里的过滤逻辑,或者在IconManager里加判断。
3.3 状态栏时钟格式与显示位置调整
时钟是状态栏上最常被定制的元素。Android 14 上,时钟的显示由Clock类控制,它支持多种格式(12 小时、24 小时、带秒、不带秒)。如果你想改时钟的显示格式,可以修改Clock里的getSmallTime()方法。
但要注意,时钟的显示位置和格式是两回事。位置由布局决定,格式由Clock类决定。我见过有人改了Clock的格式,但时钟位置没变,就以为没生效,其实是位置本来就没动。
如果你想把时钟放到状态栏左侧,除了改布局,还要注意左侧区域原本是放通知图标的。时钟移过去之后,通知图标可能会被挤掉。这时候需要调整通知图标的显示策略,比如限制通知图标的数量,或者把通知图标改成只显示一个“有通知”的指示点。
3.4 状态栏在锁屏与解锁状态下的差异处理
状态栏在锁屏和解锁状态下的表现是不一样的。锁屏状态下,状态栏通常会隐藏一些敏感图标(比如通知图标),只显示时间和系统图标。这个逻辑在StatusBar的updateStatusBarState()里处理。
如果你定制了状态栏,一定要测试锁屏下的表现。我遇到过一个典型问题:解锁状态下状态栏布局正常,锁屏状态下时钟和电量图标重叠了。原因是锁屏状态下状态栏的可用宽度计算方式不同,导致布局参数失效。解决办法是在updateStatusBarState()里根据当前状态重新设置布局参数,而不是只在初始化时设置一次。
4. 快速设置面板:磁贴重排与自定义磁贴开发
快速设置面板(QS Panel)是 SystemUI 里交互最复杂的部分之一。Android 14 上,QS 面板支持分页、磁贴拖拽、自定义磁贴等。定制需求通常集中在磁贴的排列顺序、默认显示的磁贴、以及添加自定义磁贴。
4.1 QS 面板的默认磁贴配置
Android 14 的默认磁贴配置在config.xml里,通过config_defaultQuickSettingsTiles数组定义。这个数组决定了首次开机时 QS 面板上显示哪些磁贴,以及它们的顺序。
如果你想改默认磁贴,直接改这个数组就行。但要注意,这个配置只在首次开机时生效。如果设备已经开过机,QS 面板的磁贴配置已经保存在数据库里了,改config.xml不会影响已保存的配置。测试的时候要么恢复出厂设置,要么手动清除Settings.Secure里的sysui_qs_tiles字段。
我一般的做法是:在开发阶段,每次改完默认磁贴配置后,用adb shell settings delete secure sysui_qs_tiles清除已保存的配置,然后重启 SystemUI,这样就能看到新的默认配置了。
4.2 自定义 QS 磁贴的完整开发流程
添加一个自定义磁贴,需要实现QSTile和QSTileView。下面是我实际操作的完整流程:
- 创建 Tile 类:继承
QSTileImpl,实现newTile()、handleClick()、handleUpdateState()等方法。 - 创建 TileView 类:继承
QSTileViewImpl,定义磁贴的布局和交互。 - 注册 Tile:在
QSFactoryImpl里注册你的 Tile,这样 QS 面板才能识别它。 - 添加资源:在
config.xml里添加你的 Tile 的spec名称,并确保它在默认磁贴列表里。
这里有个关键点:Android 14 的 QS 磁贴支持两种模式:单目标和双目标。单目标就是点一下切换状态,双目标是有个展开箭头,点箭头进入详细设置。如果你的磁贴需要双目标,实现方式会复杂一些,需要额外处理SecondaryTarget。
我踩过的一个坑是:自定义磁贴的图标没有着色。Android 14 的 QS 磁贴图标默认会跟随系统主题着色,如果你的图标是彩色的,可能会被强制改成单色。解决办法是在 TileView 里设置setIconTint为Color.TRANSPARENT,或者使用Icon的setTint方法手动控制。
4.3 磁贴分页与拖拽逻辑的定制
Android 14 的 QS 面板支持分页,默认是两页。如果你想改分页数量,或者禁用分页,需要修改QSPanelController里的mPages相关逻辑。
拖拽逻辑在QSPanelController和QSTileRevealController里。默认情况下,用户可以把磁贴从第二页拖到第一页,或者从第一页拖到第二页。如果你想限制某些磁贴不能被拖拽,需要在QSTileRevealController里加过滤条件。
这里有个实际经验:改分页逻辑的时候,一定要同时改QSPanelController和QSTileRevealController。只改一个地方,会出现“磁贴能拖过去但显示不出来”或者“显示出来了但拖不回来”的问题。
4.4 QS 面板展开与收起的动画调优
QS 面板的展开和收起动画是由NotificationPanelViewController控制的。Android 14 上,动画的时长和曲线在config.xml里有默认值,比如config_quickSettingsPanelExpandDuration。
如果你觉得默认动画太慢或太快,可以改这些配置值。但要注意,动画时长改得太短会导致视觉上的卡顿感,因为 SystemUI 的渲染和动画是分开的,动画太快但渲染没跟上,就会看起来一卡一卡的。我一般建议动画时长不要低于 200ms,否则在低端设备上体验会很差。
5. 定制后的验证与常见问题排查
改完 SystemUI 只是第一步,验证和排查才是真正花时间的地方。SystemUI 的问题往往不是崩溃,而是“看起来不对”——图标错位、动画卡顿、状态不同步。这些问题不会在 logcat 里报错,只能靠观察和对比。
5.1 编译与烧录后的快速验证清单
每次编译完 SystemUI 并烧录后,我会按这个清单快速过一遍:
| 检查项 | 预期表现 | 常见问题 |
|---|---|---|
| 状态栏显示 | 时间、电量、信号正常 | 图标重叠、缺失 |
| 锁屏显示 | 时钟、快捷方式正常 | 时钟错位、快捷方式不显示 |
| QS 面板 | 磁贴排列正确、可拖拽 | 磁贴丢失、拖拽失效 |
| 锁屏到解锁 | 状态栏过渡自然 | 状态栏闪烁、图标跳变 |
| AOD 显示 | 时钟与锁屏一致 | AOD 时钟不更新 |
这个清单看起来简单,但每次改完都过一遍,能省掉很多后期调试的时间。
5.2 状态栏图标错位的排查思路
状态栏图标错位是最常见的问题之一。排查的时候,我一般按这个顺序来:
- 确认布局文件是否被 overlay 覆盖:检查
device/<vendor>/<device>/overlay/下有没有 SystemUI 的 overlay,如果有,你的修改可能被覆盖了。 - 确认图标容器的宽度计算:状态栏右侧的图标容器通常是
LinearLayout,如果图标数量变化,宽度会动态调整。检查StatusBarIconController里的mIconGroups更新逻辑。 - 确认锁屏状态下的布局参数:锁屏状态下状态栏的可用宽度可能不同,检查
updateStatusBarState()里有没有重新设置布局参数。
我遇到过一次图标错位,排查了半天发现是 overlay 里有一个旧的status_bar.xml覆盖了我的修改。所以改之前先确认 overlay 情况,能省很多时间。
5.3 SystemUI 冻结或崩溃后的恢复方法
有时候改错了会导致 SystemUI 崩溃或者被系统冻结,表现就是状态栏和导航栏都消失了。这时候别慌,恢复方法有几种:
- 通过 adb 重启 SystemUI:
adb shell killall com.android.systemui,系统会自动重启 SystemUI。 - 通过 adb 清除 SystemUI 数据:
adb shell pm clear com.android.systemui,这会清除 SystemUI 的缓存和配置,恢复到默认状态。 - 如果 adb 也连不上:进入 recovery 模式,重新烧录 system 分区。
我一般会在改 SystemUI 之前,先用adb shell pm path com.android.systemui确认 APK 路径,并备份一份原始的 APK。这样即使改崩了,也能快速恢复。
5.4 媒体声音与 HDMI 相关的 SystemUI 联动问题
在 RK3576 这类板子上,插上 HDMI 后媒体声音消失是一个比较常见的问题,虽然根因通常在音频路由,但 SystemUI 的状态栏图标显示可能会受到影响。比如 HDMI 插入后,音频输出设备切换,状态栏上的音量图标可能不会及时更新。
如果你遇到类似问题,可以先确认音频路由是否正常,然后再检查 SystemUI 里的音量图标更新逻辑。Android 14 上,音量图标由StatusBarIconController管理,音频设备切换时会触发AudioManager的回调,SystemUI 需要监听这个回调并更新图标。如果 SystemUI 没有正确监听,图标就会显示错误的状态。
注意:这类问题往往不是 SystemUI 单方面的问题,而是 SystemUI 和音频服务之间的联动问题。排查的时候要同时看 audio 和 systemui 的 log。
6. 一些实际项目中的经验与建议
做了几个 SystemUI 定制项目之后,我总结了一些比较实用的经验,有些是踩坑踩出来的,有些是优化过程中发现的。
6.1 尽量用 overlay 而不是直接改源码
虽然前面说了改源码更可靠,但对于一些纯资源的修改,比如颜色、尺寸、字符串,用 RRO overlay 是更好的选择。overlay 的好处是不用重新编译 SystemUI,而且可以针对不同设备做不同的覆盖,维护起来更灵活。
我一般的策略是:逻辑改动改源码,资源改动用 overlay。这样既能保证功能正确,又能减少编译时间。
6.2 保持与原生行为的一致性
定制的时候很容易改着改着就偏离了原生行为,导致一些隐藏功能失效。比如你改了状态栏的布局,可能会影响通知的展开动画;你改了锁屏的时钟,可能会影响 AOD 的显示。
所以我的建议是:每次改动都尽量小步走,改完一个点就验证一个点,不要一次性改一大堆然后一起验证。这样出问题的时候,排查范围小,定位快。
6.3 关注 Android 14 的新特性对定制的影响
Android 14 在 SystemUI 上引入了一些新特性,比如更灵活的锁屏时钟、QS 面板的分页优化、状态栏图标的动态分组等。这些新特性在定制的时候既是机会也是坑。机会是你可以利用这些新特性做出更好的效果,坑是这些新特性的实现方式可能和你的定制逻辑冲突。
比如 Android 14 的锁屏时钟支持用户自定义样式,如果你加了一个自定义时钟但没有适配这个机制,用户切换样式时你的时钟就会消失。所以定制之前,先花点时间了解 Android 14 在 SystemUI 上的新变化,能避免很多返工。
6.4 调试工具与日志的合理使用
SystemUI 的调试,光靠看界面是不够的,还得结合日志。我常用的几个命令:
# 查看 SystemUI 的日志 adb logcat -s SystemUI # 查看状态栏图标的更新日志 adb logcat -s StatusBarIconController # 查看 QS 面板的日志 adb logcat -s QSPanelController # dump SystemUI 的状态 adb shell dumpsys systemuidumpsys systemui特别有用,它会输出当前 SystemUI 的详细状态,包括状态栏图标、QS 磁贴、锁屏状态等。排查问题的时候,先 dump 一下,往往能快速定位到问题所在。
6.5 版本管理与回滚策略
SystemUI 的定制往往不是一次性的,而是随着需求变化不断调整。所以版本管理很重要。我一般会用 git 管理 SystemUI 的修改,每次改动都提交一次,并写清楚改了什么、为什么改。这样出问题的时候可以快速回滚到上一个稳定版本。
另外,建议在每次重大改动之前,先打一个 tag,标记为稳定版本。这样即使改崩了,也能快速恢复到稳定状态,不至于影响整体进度。
最后再分享一个小技巧:如果你在改 SystemUI 的时候不确定某个效果是不是自己改出来的,可以先把改动还原,编译一个原生版本对比一下。很多时候问题不是你的改动引起的,而是原生就有的行为,只是你之前没注意到。这个对比法帮我省了不少瞎折腾的时间。