6、多屏显示与窗口管理:Android Automotive多屏架构
多屏显示,是车载系统里最绕不开的话题。你想想看,一辆车里少说两三块屏,多的五六块都有。仪表盘、中控、副驾屏、后排娱乐屏……每块屏各司其职,但又得协同工作。Android Automotive 从底层就支持多屏,但真正用好它,还是有不少门道的。
我个人习惯把多屏系统比作一个「剧院」:DisplayManager 是舞台总监,WindowManager 是灯光师,SystemUI 是场务。各司其职,缺一不可。下面我们一层层拆开来看。
6.1 多屏架构:DisplayManager 与 WindowManager
先说 DisplayManager。它是 Android 系统里管理物理显示器的核心服务。每块屏幕接入系统,DisplayManager 都会给它分配一个唯一的 Display ID。这个 ID 很重要,后面所有窗口操作都离不开它。
核心概念:DisplayManager 负责「发现」和「配置」屏幕,WindowManager 负责「往屏幕上放窗口」。两者配合,才能实现多屏显示。
WindowManager 在多屏环境下的工作方式,和单屏有很大区别。单屏时,我们只需要关心一个窗口栈。多屏时,每个 Display 都有自己的窗口栈。也就是说,中控屏上的应用切换,不会影响仪表盘的显示。
我在项目中遇到过一个问题:副驾屏上的视频应用全屏播放时,中控屏的导航窗口突然被挤到后面去了。排查了半天,发现是 WindowManager 的窗口策略没配好。说白了,就是没告诉系统「副驾屏的窗口栈和中控屏的窗口栈是独立的」。
解决方式很简单,在配置里声明每个 Display 的窗口栈策略。代码示例如下:
// 为每个 Display 创建独立的窗口栈
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
// 获取 Display 的上下文
Context displayContext = context.createDisplayContext(display);
// 在指定 Display 上启动 Activity
Intent intent = new Intent(context, VideoPlayerActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
displayContext.startActivity(intent);
小提示:FLAG_ACTIVITY_MULTIPLE_TASK 这个 flag 很关键。不加它,系统可能会把新窗口塞到已有窗口栈里,导致显示错乱。
6.2 多屏策略与配置:DisplayInfo 和 DisplayDevice
每块屏幕都有自己的「身份证」—— DisplayInfo。它记录了屏幕的尺寸、密度、刷新率、旋转角度等信息。拿到这些信息,我们才能决定怎么布局。
举个例子,仪表盘通常是 1280x480 的细长屏,中控是 1920x1080 的横屏,副驾可能是 1920x720 的宽屏。三块屏的 DPI 也可能不一样。如果不做适配,同一个 UI 组件在三块屏上显示效果会差很多。
我建议在系统启动时,先枚举所有 Display,然后根据 DisplayInfo 做差异化配置:
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = dm.getDisplays();
for (Display display : displays) {
DisplayInfo info = new DisplayInfo();
display.getDisplayInfo(info);
// 根据屏幕类型做不同处理
if (info.displayId == Display.DEFAULT_DISPLAY) {
// 主屏(中控)配置
setupMainDisplay(info);
} else if (isInstrumentCluster(display)) {
// 仪表盘配置
setupClusterDisplay(info);
} else {
// 副驾或其他屏幕
setupSecondaryDisplay(info);
}
}
DisplayDevice 是更底层的概念。它代表物理显示设备,比如一块 LCD 面板。在 Android Automotive 里,系统通过 Hardware Composer 和 DisplayDevice 交互。这块一般由硬件厂商实现,应用层开发者接触不多。但了解它的存在,有助于理解为什么有些屏幕配置改不了——因为 DisplayDevice 的某些属性是硬件锁死的。
注意:不要试图在运行时动态修改 DisplayDevice 的物理参数(如分辨率、刷新率)。我曾经试过,结果屏幕直接黑掉了。这些参数必须在系统启动前通过设备树或内核参数配置好。
6.3 SystemUI 在多屏环境下的适配
SystemUI 是 Android 系统的「门面」——状态栏、导航栏、通知面板都是它管的。单屏时,SystemUI 只在一个 Display 上显示。多屏时,问题就来了:每块屏都需要自己的状态栏吗?导航栏放哪块屏上?
嗯,这里要注意。Android Automotive 的默认做法是:只有主屏(中控)显示 SystemUI。仪表盘和副驾屏通常不显示系统栏,而是由车载厂商自定义 UI。
但有些场景需要特殊处理。比如副驾屏上运行了第三方应用,用户需要返回键。这时候,要么在副驾屏上显示一个半透明的导航栏,要么通过手势操作。
我在项目中做过一个方案:为副驾屏单独启动一个 SystemUI 实例,但只显示导航栏,不显示状态栏。实现方式是在 SystemUI 的启动配置里,指定它绑定到哪个 Display:
// SystemUI 启动时,指定 Display
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = dm.getDisplay(Display.DEFAULT_DISPLAY); // 或者指定其他 Display ID
// 创建绑定到特定 Display 的 Context
Context displayContext = context.createDisplayContext(display);
// 启动 SystemUI 服务
Intent intent = new Intent(displayContext, SystemUIService.class);
displayContext.startService(intent);
经验之谈:多屏 SystemUI 适配最头疼的是焦点管理。用户在中控屏上操作时,副驾屏的 SystemUI 不能抢焦点。我建议用 InputMonitor 来拦截不必要的触摸事件,只让当前活跃屏幕的 SystemUI 响应输入。
6.4 车载多屏场景实战
聊完理论,我们看看实际场景。一辆典型的智能汽车,至少有三块屏:仪表盘、中控、副驾屏。每块屏的职责不同,窗口管理策略也不同。
| 屏幕 | 典型分辨率 | 主要功能 | 窗口策略 |
|---|---|---|---|
| 仪表盘 | 1280x480 | 车速、导航、告警 | 全屏独占,不允许其他窗口覆盖 |
| 中控 | 1920x1080 | 导航、音乐、设置 | 多窗口自由切换,支持分屏 |
| 副驾屏 | 1920x720 | 视频、游戏、社交 | 独立窗口栈,不影响主屏 |
仪表盘的窗口管理最严格。因为它涉及行车安全,不能有任何应用窗口遮挡。我建议把仪表盘设为「安全显示区域」,只允许系统级应用(如仪表盘应用)在上面绘制。其他应用一律禁止。
中控屏是「主战场」。导航、音乐、电话、设置都在这里。窗口策略要支持分屏和画中画。比如导航应用占上半屏,音乐应用占下半屏。Android Automotive 的 Multi-Window 模式在这里很实用。
副驾屏相对自由。乘客可以看视频、玩游戏,甚至投屏。但要注意,副驾屏上的音频不能干扰驾驶员的导航提示。我建议用 AudioFocus 机制来管理音频焦点,副驾屏播放视频时,自动降低导航音量。
下面这张图展示了三块屏的窗口管理架构:
从架构图可以看出,三块屏通过 DisplayManager 统一管理,WindowManager 为每块屏分配独立的窗口栈。系统服务层负责协调,硬件层负责最终渲染。这种分层设计,让每块屏都能独立工作,互不干扰。
实战建议:开发多屏应用时,先用 adb 命令查看当前系统的 Display 列表:adb shell dumpsys display。这个命令会列出所有 Display 的 ID、分辨率、密度等信息。拿到这些数据,你才能针对性地做适配。
最后说一句,多屏开发最忌讳「想当然」。你以为副驾屏的窗口不会影响中控屏,但实际测试时发现焦点被抢了、音频串了、触摸事件被拦截了……这些问题都很常见。我的经验是:每块屏单独测试,然后做交叉测试。尤其是焦点管理和音频焦点,一定要反复验证。
好了,多屏显示与窗口管理就聊到这里。这部分内容在实际项目中占比很大,建议你动手写个 Demo,在三块屏上分别跑不同的应用,感受一下窗口栈的独立性。