用大白话了解 SystemUI 的原理(含Demo源码:Compose+Kotlin)

41 阅读5分钟

kotlin+compose版本的SystemUI Demo,包含状态栏,导航栏,快捷面板,全局手势、最近任务等 魔法之路(1)_000000_000005_20261014-142130.gif

我用最直白,最不绕弯子 的简单原理给你说清楚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.heightSystemUI 这个窗口有多高
状态栏 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 通常这样分工:

  • SystemUI:接收最近任务按钮操作,通知桌面。入口实现
  • Launcher3 的 Quickstep:显示任务卡片,处理滑动和动画。界面实现
  • 系统任务服务:提供任务列表,执行切换和移除。任务服务

用最简单的 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的。