2. 显示系统基础:DisplayManager 与 Display 类详解、物理屏与逻辑屏的概念、多屏配置与热插拔处理

各位同学,欢迎来到第二章。这一章我们聊聊显示系统的基础。

说实话,车载多屏交互的根基,就是搞懂 Android 的显示框架。你想想看,车上有仪表盘、中控屏、副驾屏,甚至还有 HUD,这些屏幕怎么被系统识别?怎么管理?怎么在插拔时不出乱子?

嗯,今天我们就从 DisplayManagerDisplay 这两个核心类入手,把物理屏和逻辑屏的概念掰开揉碎,再聊聊多屏配置和热插拔的那些坑。

2.1 DisplayManager:屏幕的总管家

DisplayManager 是什么?说白了,它就是系统里所有屏幕的「大管家」。你通过它,可以获取当前连接的屏幕列表、监听屏幕的插拔事件、甚至控制屏幕的显示模式。

我个人习惯,在车载项目里,第一件事就是拿到这个服务:

DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);

拿到之后,你能干什么?我列几个最常用的操作:

  • 获取所有屏幕getDisplays() 返回所有可用的 Display 对象。
  • 按类别获取getDisplays(String category),比如只拿副屏或者只拿主屏。
  • 监听变化:注册 DisplayListener,屏幕一插一拔,你立刻知道。

这里有个细节要注意。在 Android Automotive 里,DisplayManager 的行为和手机端不太一样。手机通常只有一个内置屏,但车机可能有 3 个、4 个甚至更多。所以,千万别用手机端的思维去写车机代码。

我的经验: 在项目初期,我建议你先把所有屏幕的 Display ID 打印出来。我曾经在一个项目里,发现系统返回了 5 个 Display,但实际物理屏只有 3 个。剩下的两个是虚拟屏,用于投屏或者录制。如果不搞清楚,后面会踩大坑。

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 里,多屏配置主要通过 DisplayDeviceConfigDisplayManagerService 来完成。不过,作为应用层开发者,你更关心的是怎么读取这些配置。

我常用的做法是,在系统启动时,遍历所有屏幕,然后根据 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_DOZESTATE_OFF。你的应用需要根据状态来决定是否继续渲染内容,否则会浪费系统资源。

2.6 小结与个人建议

好了,这一章的内容就到这里。我们聊了 DisplayManagerDisplay 的核心用法,区分了物理屏和逻辑屏,也讲了多屏配置和热插拔的处理。

最后,我分享一个个人习惯:在车载项目的初期,我会写一个「屏幕诊断工具」,把当前所有屏幕的 ID、名称、分辨率、状态、标志位全部打印出来。这个工具在调试多屏交互时,能帮你省下大量时间。

下一章,我们会深入 SurfaceControlWindowManager,看看怎么把内容真正「画」到这些屏幕上。到时候见。