12、多屏与多窗口:DisplayManager与多屏支持、Presentation与副屏、多窗口模式、分屏适配
车机现在越来越像一台移动的平板电脑了。你想想看,中控一个屏,仪表一个屏,后排可能还有娱乐屏。甚至有些高端车型,副驾前面也给你塞一块屏幕。这么多屏幕,怎么让它们各司其职?这就是我们今天要聊的核心。
我个人习惯把多屏开发比作「搭积木」。每块屏幕是一个独立的显示空间,但底层又共享同一个系统资源。Android Automotive OS 在这方面做得挺成熟,但坑也不少。我踩过几次,今天一并倒出来。
DisplayManager:屏幕的「户口本」
在 Android Automotive OS 里,每一块屏幕都被抽象成一个 Display 对象。而 DisplayManager 就是管理这些对象的管家。你可以通过它拿到所有可用的屏幕,包括内屏、外屏、甚至虚拟屏。
我刚开始做车机项目时,以为只有一块屏幕。结果一查,好家伙,三块。代码怎么写?很简单:
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = dm.getDisplays();
for (Display display : displays) {
Log.d("AutoDisplay", "Display ID: " + display.getDisplayId()
+ ", Name: " + display.getName()
+ ", Flags: " + display.getFlags());
}
这里有个细节要注意。仪表屏通常有 FLAG_PRIVATE 标志,普通应用是拿不到的。我曾经在调试时死活找不到仪表屏,后来才发现是权限问题。嗯,车机的安全策略比手机严格得多。
Presentation:副屏的「专属舞台」
拿到副屏的 Display 对象后,怎么在上面显示内容?直接用 Activity 吗?不行。副屏没有自己的 Activity 栈。正确的做法是用 Presentation。
Presentation 本质上是一个特殊的 Dialog,但它绑定了一个特定的 Display。你给它一个 Context 和一个 Display,它就在那块屏幕上弹出来。我习惯把它叫做「副屏的专属舞台」。
public class RearSeatPresentation extends Presentation {
public RearSeatPresentation(Context context, Display display) {
super(context, display);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_rear_seat);
// 初始化副屏的 UI
TextView tv = findViewById(R.id.rear_message);
tv.setText("后排娱乐已启动");
}
}
// 在 Activity 中启动
Display rearDisplay = findRearDisplay(); // 自己实现查找逻辑
if (rearDisplay != null) {
RearSeatPresentation presentation = new RearSeatPresentation(this, rearDisplay);
presentation.show();
}
这里有个大坑:Presentation 的 Context 必须来自主屏的 Activity,不能是 Application Context。为什么?因为 Presentation 需要依赖 Activity 的窗口令牌(token)。我当初图省事用了 Application Context,结果副屏死活弹不出来,日志报错说「Bad token」。折腾了半天才找到原因。
多窗口模式:车机上的「分屏艺术」
车机上的多窗口和手机不太一样。手机分屏通常是上下或左右各一半。车机呢?中控屏可能左边是导航,右边是音乐。仪表屏可能左边是车速,右边是地图。甚至有些车支持自由窗口,像桌面系统一样拖拽。
Android Automotive OS 从 API 29 开始就原生支持多窗口模式。但车厂往往有自己的定制。我参与的项目里,分屏比例通常是 7:3 或 6:4,很少用 1:1。因为导航需要更多空间。
适配多窗口,核心是处理配置变化。当窗口大小改变时,Activity 会重建。你需要在 onConfigurationChanged() 里重新布局。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 判断当前是否处于多窗口模式
if (isInMultiWindowMode()) {
// 切换到紧凑布局
showCompactUI();
} else {
// 恢复全屏布局
showFullUI();
}
}
我个人习惯在 AndroidManifest.xml 里给 Activity 加上 android:resizeableActivity="true"。这样系统就知道你的应用支持多窗口。如果不加,系统会强制把你的应用放在一个固定大小的窗口里,用户体验很差。
getResources().getConfiguration().smallestScreenWidthDp 会变小。你可以用这个值来判断当前是分屏还是全屏。比如小于 600dp 就认为是分屏状态。
分屏适配:从布局到生命周期
分屏适配不只是改改布局文件。生命周期也要注意。当应用进入分屏模式时,Activity 会经历 onPause() 和 onStop()。但注意,如果另一个窗口是可见的,你的 Activity 可能仍然处于 onStart() 状态,不会完全销毁。
我遇到过一个问题:音乐应用在分屏时,用户切换到导航窗口,音乐播放器居然停了。查了半天,发现是 onPause() 里调了 pauseMusic()。后来改成只在 onStop() 里暂停,并且判断 isInMultiWindowMode() 为 false 时才暂停。分屏模式下只降低音量,不暂停。
@Override
protected void onPause() {
super.onPause();
if (!isInMultiWindowMode()) {
// 全屏退出时才暂停
pauseMusic();
} else {
// 分屏时只降低音量
lowerVolume();
}
}
布局方面,我建议用 ConstraintLayout 配合 Guideline。这样在窗口大小变化时,控件能自动调整位置。不要用绝对坐标,也不要用 LinearLayout 的固定权重,除非你确定窗口比例不会变。
| 场景 | 布局策略 | 生命周期注意事项 |
|---|---|---|
| 全屏模式 | 大图、大字体、多列 | 正常 onPause/onResume |
| 分屏模式(主窗口) | 单列、紧凑、可滚动 | onPause 时不释放资源 |
| 分屏模式(副窗口) | 极简、只显示核心信息 | onStop 时保存状态 |
| 自由窗口模式 | 可拖拽、可缩放 | onConfigurationChanged 频繁触发 |
最后说一句,多屏和多窗口的调试比较麻烦。模拟器只能模拟一块屏幕,真车又不好借。我建议用 adb shell dumpsys display 查看当前屏幕列表,再用 wm size 和 wm density 模拟不同分辨率。虽然不能完全替代真机,但能解决 80% 的问题。
好了,这一章的内容就这些。多屏开发其实不难,难的是想清楚每块屏幕的职责。记住:主屏负责交互,副屏负责展示,分屏负责共存。把这三件事理清了,代码自然就顺了。