kotlin+compose版本的SystemUI Demo,包含状态栏,导航栏,快捷面板,全局手势、最近任务等
我用最直白,最不绕弯子 的简单原理给你说清楚SystemUI,我这里使用状态栏作为栗子。
其实,SystemUI也是应用,和普通应用不同的是,它拥有系统签名,还有 AndroidManifest.xml 的 android:sharedUserId="android.uid.system"。
🚀 如何创建的状态栏界面
以下代码就实现了一个 最简单的状态栏,抛开所有状态栏的复杂代码。StatusBarWindowController源码
说白了, mWindowManager 通过调用 addView,将UI界面显示出来而已,只不过它的窗口类型是TYPE_STATUS_BAR。
// 1. 创建 UI
val view = TextView(context).apply {
text = "12:00 Wi-Fi 100%"
setTextColor(Color.WHITE)
setBackgroundColor(Color.BLACK)
gravity = Gravity.CENTER_VERTICAL
}
// 2. 描述窗口
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
statusBarHeightPx,
WindowManager.LayoutParams.TYPE_STATUS_BAR, // 状态栏窗口类型
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP
}
// 3. 添加窗口
val wm = context.getSystemService(WindowManager::class.java)
wm.addView(view, params)
🚀 如何将状态栏的高度告诉系统
| 数值 | 含义 |
|---|---|
| 窗口高度 LayoutParams.height | SystemUI 这个窗口有多高 |
| 状态栏 Insets | 告诉其它应用窗口,顶部有多大的状态栏区域 |
lp.height = height;
// Insets.of(0, height, 0, 0) 的四个值依次是左、上、右、下,因此这里表示:
// 这个窗口提供一个状态栏区域,位于顶部,高度是 height。
lp.providedInsets = new InsetsFrameProvider[] {
new InsetsFrameProvider(owner, 0, statusBars())
.setInsetsSize(Insets.of(0, height, 0, 0))
};
windowManager.addView(statusBarView, lp);
系统侧的 DisplayPolicy.addWindowLw() 收到窗口后,会:
- 根据 TYPE_STATUS_BAR,记住这是状态栏窗口。
- 读取 providedInsets,建立对应的 Insets 来源。
因此,系统知道的不只是“有一个窗口”,还知道“这个窗口提供顶部状态栏区域”。DisplayPolicy 源码
窗口高度和 Insets 可以不同。 例如状态栏窗口为了动画临时铺满屏幕,Insets 仍然只代表顶部那条状态栏,避免让应用误以为整屏都需要避让。
🚀 如何修改状态栏的背景以及图标颜色
上层:SystemUI 布局出 时间、信号、电量 等图标
下层:应用窗口画状态栏区域的背景
1.应用请求的背景颜色常见情况下由应用的 PhoneWindow、DecorView 处理
应用侧的调用
window.statusBarColor = Color.RED
你看到的红色背景,通常是应用窗口画出来的。
透明状态栏场景下,也可能直接露出应用延伸到顶部的内容;SystemUI 在锁屏等场景还有自己的背景处理。
2.显示或隐藏状态栏
应用通过 WindowInsetsController.show()/hide() 请求,系统协调处理
window.insetsController?.hide(WindowInsets.Type.statusBars())
window.insetsController?.show(WindowInsets.Type.statusBars())
这是 Insets 控制流程,不能简单理解为应用直接对 SystemUI 的 View 调用 GONE。InsetsController 显隐源码
3.图标明暗应用请求外观,系统通知 SystemUI
应用侧: LIGHT_STATUS_BARS 指适合浅色背景的状态栏外观,通常表现为深色图标。
val flag = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS // 深色图标,适合浅色背景
window.insetsController?.setSystemBarsAppearance(flag, flag) // 切回浅色图标
window.insetsController?.setSystemBarsAppearance(0, flag)
SystemUI是如何被系统通知的?
// SystemUI 启动时,实际监听系统的源码就这一句
// 系统以后有状态栏命令,请通过 mCommandQueue 通知我。
mBarService.registerStatusBar(mCommandQueue);
// system_server 进程,这块代码在 frameworks 里面.
// 这就是为何很多人感觉SystemUI代码很复杂的原因,
// 某些逻辑和frameworks有一些挂钩,其实抛开复杂的逻辑,就是简单的监听回调而已。
class StatusBarManagerService {
IStatusBar mBar;
void registerStatusBar(IStatusBar callback) {
mBar = callback; // 保存通往 SystemUI 的 Binder 接口
}
}
真实代码中的核心也是 mBar = bar。StatusBarManagerService 源码
WMS 中的 DisplayPolicy 根据当前窗口决定系统栏外观,再通知状态栏服务。DisplayPolicy 源码
状态栏服务的转发逻辑可以简化成:
// system_server 进程,伪代码
void onSystemBarAttributesChanged(int appearance) {
if (mBar != null) {
// Binder 跨进程调用:从系统服务进入 SystemUI
mBar.onSystemBarAttributesChanged(appearance);
}
}
这一行调用,就是 SystemUI 收到通知的关键。 真实方法还传递显示屏编号、各区域外观等参数。系统服务转发StatusBarManagerService源码
SystemUI 的 CommandQueue 接收,再切到主线程
收到 Binder 调用后,CommandQueue 把事件交给主线程处理。
// SystemUI 进程,伪代码
class CommandQueue extends IStatusBar.Stub {
Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
public void onSystemBarAttributesChanged(int appearance) {
// Binder 接收端:先把通知送到主线程
mainHandler.post(() -> {
lightBarController.onStatusBarAppearanceChanged(appearance);
});
}
}
原生实现用 Handler 消息分发给内部回调;上面的代码合并了这些转发层。CommandQueue 接收与分发源码
读出标志,更新图标
// SystemUI 进程,伪代码
void onStatusBarAppearanceChanged(int appearance) {
boolean darkIcons = (appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0;
transitionsController.setIconsDark(darkIcons, false);
}
设置标志时使用深色图标,清除标志时使用浅色图标。原生代码会结合状态栏模式和各个区域分别判断。LightBarController 源码
最近任务
最近原理其实很简单,就是获取的 当前运行的应用列表,显示到界面上。
剩下的,就是 获取应用不显示标志、滑动关闭应用、点击启动应用等一些逻辑而已。
现代原生 Android 通常这样分工:
用最简单的 Kotlin + Compose 伪代码表示:
// 用户打开最近任务
fun showRecents() {
val tasks = system.getRecentTasks()
tasks.forEach { task ->
TaskCard(
title = task.title,
image = task.snapshot, // 系统保存的画面
onClick = {
system.openTask(task.id) // 切回这个任务
},
onSwipeUp = {
system.removeTask(task.id) // 移除这个任务
}
)
}
}
结尾
从以上文章分析 + 代码demo,这些都是SystemUI最基本的逻辑,其它的东西都是在这个基础上慢慢加上去的.
按照现在的业务来说,你不搞手机版的SystemUI,比如车载和掌机、电视、投影等定制化的SystemUI来说,基本可以删除掉原生的SystemUI, 按照业务重写一个全新的SystemUI是完全OK的。