2. 显示系统基础:DisplayManager 与 Display 类详解、物理屏与逻辑屏的概念、多屏配置与热插拔处理
各位同学,欢迎来到第二章。这一章我们聊聊显示系统的基础。
说实话,车载多屏交互的根基,就是搞懂 Android 的显示框架。你想想看,车上有仪表盘、中控屏、副驾屏,甚至还有 HUD,这些屏幕怎么被系统识别?怎么管理?怎么在插拔时不出乱子?
嗯,今天我们就从 DisplayManager 和 Display 这两个核心类入手,把物理屏和逻辑屏的概念掰开揉碎,再聊聊多屏配置和热插拔的那些坑。
2.1 DisplayManager:屏幕的总管家
DisplayManager 是什么?说白了,它就是系统里所有屏幕的「大管家」。你通过它,可以获取当前连接的屏幕列表、监听屏幕的插拔事件、甚至控制屏幕的显示模式。
我个人习惯,在车载项目里,第一件事就是拿到这个服务:
DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
拿到之后,你能干什么?我列几个最常用的操作:
- 获取所有屏幕:
getDisplays()返回所有可用的 Display 对象。 - 按类别获取:
getDisplays(String category),比如只拿副屏或者只拿主屏。 - 监听变化:注册
DisplayListener,屏幕一插一拔,你立刻知道。
这里有个细节要注意。在 Android Automotive 里,DisplayManager 的行为和手机端不太一样。手机通常只有一个内置屏,但车机可能有 3 个、4 个甚至更多。所以,千万别用手机端的思维去写车机代码。
2.2 Display 类:屏幕的身份证
每个屏幕在系统里,都是一个 Display 对象。它就像屏幕的「身份证」,记录了屏幕的所有关键信息。
来看看它最重要的几个属性:
| 属性 | 说明 | 我常用的场景 |
|---|---|---|
getDisplayId() |
屏幕的唯一 ID | 区分主屏和副屏 |
getFlags() |
屏幕的标志位 | 判断是否为主屏、是否支持投屏 |
getMetrics() |
屏幕的尺寸、密度 | 适配不同分辨率的屏幕 |
getState() |
屏幕状态(开/关/待机) | 判断屏幕是否可用 |
举个例子,你想知道某个屏幕是不是主屏,可以这样判断:
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
if ((display.getFlags() & Display.FLAG_PRIMARY) != 0) {
// 这是主屏
}
嗯,这里要注意,FLAG_PRIMARY 这个标志位,在车机上不一定只有一个。有些车厂会把仪表屏也标记为主屏,这就容易出问题。我建议你拿到标志位后,先按位与一下,别直接等号判断。
2.3 物理屏 vs 逻辑屏:别搞混了
这个概念,很多初学者会绕晕。我简单解释一下:
- 物理屏:就是实实在在的硬件屏幕。比如你车上的那块 12.3 寸的液晶仪表盘。
- 逻辑屏:是系统在软件层面虚拟出来的屏幕。它可能对应一个物理屏,也可能不对应任何物理屏。
为什么会这样?你想想看,Android 系统需要把 UI 内容渲染到某个「画布」上,这个画布就是逻辑屏。一个物理屏可以对应多个逻辑屏(比如分屏显示),多个物理屏也可以合并成一个逻辑屏(比如拼接屏)。
在车载场景里,最常见的逻辑屏是 虚拟显示(VirtualDisplay)。它不直接对应任何物理硬件,而是把内容投射到其他地方,比如通过 Wi-Fi 投屏到后排娱乐屏。
2.4 多屏配置:车机特有的玩法
手机通常只有一个屏,配置很简单。但车机不一样,每个屏幕的分辨率、刷新率、DPI 都可能不同。怎么配置?
在 Android Automotive 里,多屏配置主要通过 DisplayDeviceConfig 和 DisplayManagerService 来完成。不过,作为应用层开发者,你更关心的是怎么读取这些配置。
我常用的做法是,在系统启动时,遍历所有屏幕,然后根据 Display ID 建立映射关系:
Display[] displays = displayManager.getDisplays();
for (Display display : displays) {
int id = display.getDisplayId();
String name = display.getName();
Display.Metrics metrics = new Display.Metrics();
display.getMetrics(metrics);
Log.d("MultiDisplay", "屏幕 ID: " + id + ", 名称: " + name +
", 分辨率: " + metrics.widthPixels + "x" + metrics.heightPixels);
}
这段代码看起来简单,但我在项目里吃过亏。有一次,某个屏幕的 getName() 返回的是空字符串,导致后续的 UI 映射全乱了。后来发现,是系统服务层没有正确初始化屏幕名称。所以,我建议你拿到 Display 对象后,先做一次空值检查。
2.5 热插拔处理:别让用户看到黑屏
热插拔,就是屏幕在系统运行过程中被插入或拔出。这在车机上很常见,比如用户外接了一个后排娱乐屏,或者某个屏幕的线缆松动。
处理热插拔,核心是注册 DisplayListener:
displayManager.registerDisplayListener(new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
// 屏幕插入
Log.d("HotPlug", "屏幕 " + displayId + " 已插入");
// 重新分配 UI 内容
}
@Override
public void onDisplayRemoved(int displayId) {
// 屏幕拔出
Log.d("HotPlug", "屏幕 " + displayId + " 已拔出");
// 把该屏幕上的内容迁移到其他屏幕
}
@Override
public void onDisplayChanged(int displayId) {
// 屏幕属性变化(比如分辨率变了)
Log.d("HotPlug", "屏幕 " + displayId + " 属性已变更");
// 重新适配 UI
}
}, null);
onDisplayRemoved 事件。结果用户拔掉副屏后,原本显示在副屏上的导航界面直接消失了,主屏也没有接管。用户投诉说「导航丢了」。从那以后,我每次都会在热插拔回调里,做一次 UI 内容的重新分配。
另外,热插拔时还有一个容易忽略的点:屏幕状态变化。比如屏幕虽然物理连接着,但进入了休眠状态。这时候 getState() 会返回 STATE_DOZE 或 STATE_OFF。你的应用需要根据状态来决定是否继续渲染内容,否则会浪费系统资源。
2.6 小结与个人建议
好了,这一章的内容就到这里。我们聊了 DisplayManager 和 Display 的核心用法,区分了物理屏和逻辑屏,也讲了多屏配置和热插拔的处理。
最后,我分享一个个人习惯:在车载项目的初期,我会写一个「屏幕诊断工具」,把当前所有屏幕的 ID、名称、分辨率、状态、标志位全部打印出来。这个工具在调试多屏交互时,能帮你省下大量时间。
下一章,我们会深入 SurfaceControl 和 WindowManager,看看怎么把内容真正「画」到这些屏幕上。到时候见。