3. 多屏显示策略:Presentation 类与 Dialog 的使用、多屏 Activity 启动模式、跨屏拖拽与焦点管理
好,咱们进入第三个核心话题。多屏显示,说白了就是让车上的几块屏幕各司其职,别打架。我最早接触这个的时候,以为就是把 Activity 扔到不同屏幕上就完事了。结果呢?焦点乱跳、Dialog 显示错位、拖拽卡顿……踩了不少坑。今天我把这些经验掰开揉碎讲给你。
3.1 Presentation 类:副屏的专属舞台
Android 里有个专门的类叫 Presentation,它就是为副屏量身定做的。你想想看,主屏跑着导航,副屏放音乐,如果用同一个 Activity 去控制,逻辑会乱成一锅粥。
Presentation 本质上是一个特殊的 Dialog,但它绑定了一个特定的 Display 对象。创建时,你需要传入目标屏幕的 Display 信息。
核心要点: Presentation 的生命周期由它依附的 Activity 管理,但它的 UI 渲染在目标屏幕上独立进行。
// 获取副屏 Display
DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = displayManager.getDisplays();
Display secondaryDisplay = displays[1]; // 假设第二块屏
// 创建 Presentation
MediaPresentation presentation = new MediaPresentation(this, secondaryDisplay);
presentation.show();
// 自定义 Presentation 类
class MediaPresentation extends Presentation {
public MediaPresentation(Context context, Display display) {
super(context, display);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_media);
// 初始化副屏 UI
}
}
我在项目中遇到过一个问题:Presentation 里的点击事件没反应。查了半天,发现是焦点没过去。嗯,这里要注意,Presentation 默认不获取焦点,你得手动处理。
个人习惯: 我一般会在 Presentation 的根布局加上 android:focusable="true" 和 android:focusableInTouchMode="true",省得后面出幺蛾子。
3.2 Dialog 在多屏下的“迷路”问题
普通 Dialog 默认显示在主屏上。如果你在副屏上操作,突然弹出一个 Dialog 跑到主屏去了,用户体验会非常割裂。为什么会这样?因为 Dialog 的 Window Token 绑定的是启动它的 Activity 所在的屏幕。
解决办法有两个:
- 方法一: 使用
Dialog.setOwnerActivity()指定归属 Activity,但这个方法对跨屏无效。 - 方法二: 直接用
Presentation替代 Dialog。说白了,副屏上的弹窗就用 Presentation 来实现。
我曾经踩过的坑: 在副屏上弹了一个 AlertDialog,结果用户在主屏上看到了。用户一脸懵,以为系统出 bug 了。后来我统一规定:副屏上的所有弹窗,必须用 Presentation 的子类来实现。
// 副屏弹窗的正确姿势
class AlertPresentation extends Presentation {
public AlertPresentation(Context context, Display display) {
super(context, display);
// 设置 Dialog 风格
setTheme(android.R.style.Theme_Material_Light_Dialog);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_alert);
// 你的弹窗逻辑
}
}
3.3 多屏 Activity 启动模式
Activity 在多屏环境下的启动模式,和单屏有很大区别。你想想看,如果两个屏幕同时启动同一个 Activity 的多个实例,会怎样?
Android 提供了 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_MULTIPLE_TASK 这两个关键 Flag。配合 ActivityOptions.setLaunchDisplayId(),你可以精确控制 Activity 启动到哪块屏幕上。
| 启动模式 | 行为说明 | 适用场景 |
|---|---|---|
| singleTask + 指定 Display | 在目标屏幕上创建新 Task,复用已有实例 | 导航、音乐等常驻应用 |
| standard + NEW_TASK | 每次都在新 Task 中创建实例 | 临时工具、设置页面 |
| singleInstance + 指定 Display | 独占一个 Task,且只能在该屏幕运行 | 视频播放、全屏游戏 |
// 启动 Activity 到指定屏幕
Intent intent = new Intent(this, MediaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(secondaryDisplay.getDisplayId());
startActivity(intent, options.toBundle());
避坑指南: 我曾经在副屏上启动了一个 singleTask 的 Activity,结果主屏上的同一个 Activity 被覆盖了。后来我加上了 FLAG_ACTIVITY_MULTIPLE_TASK,才让两个屏幕各自独立运行。
3.4 跨屏拖拽:从主屏拖到副屏
跨屏拖拽是车载多屏交互的亮点功能。比如把主屏上的音乐卡片拖到副屏上播放。Android 的拖拽框架 DragAndDrop 支持跨 Display 操作,但需要额外处理。
核心思路是:
- 源端:启动拖拽时,传入
ClipData和DragShadowBuilder - 目标端:监听
onDragEvent,接收拖拽数据 - 跨屏传递:通过
DragAndDropPermissions授权
// 启动拖拽
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDragAndDrop(clipData, shadowBuilder, null, 0);
// 目标 View 接收
view.setOnDragListener((v, event) -> {
switch (event.getAction()) {
case DragEvent.ACTION_DROP:
ClipData.Item item = event.getClipData().getItemAt(0);
String data = item.getText().toString();
// 处理拖拽数据
return true;
}
return false;
});
我的经验: 跨屏拖拽时,阴影的绘制是个坑。默认的 DragShadow 只会在源屏幕显示。我建议自定义 DragShadowBuilder,把阴影绘制在全局坐标中,这样用户拖到屏幕边缘时,阴影能平滑过渡。
3.5 焦点管理:谁在控制权?
多屏环境下,焦点管理是最容易出问题的。你想想看,主屏的导航在输入目的地,副屏的音乐在搜索歌曲,两个输入法同时弹出?那画面太美我不敢看。
Android 的焦点系统是基于 Window 的。每个 Display 有自己的焦点 Window。但问题是,系统级的焦点(比如按键事件)只有一个。
我的建议是:
- 明确主从关系: 主屏拥有系统焦点控制权,副屏的焦点事件通过 IPC 转发
- 使用
View.requestFocus(): 在 Presentation 中主动请求焦点 - 监听焦点变化: 通过
View.OnFocusChangeListener感知焦点状态
// 在 Presentation 中请求焦点
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_media);
View rootView = findViewById(android.R.id.content);
rootView.setFocusable(true);
rootView.setFocusableInTouchMode(true);
rootView.requestFocus();
rootView.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
// 通知系统,副屏获得焦点
notifyFocusChanged(true);
}
});
}
我曾经遇到的严重问题: 副屏上的 EditText 获取焦点后,主屏的输入法被抢走了。用户正在导航输入,突然键盘没了。解决方案是:副屏上的文本输入,使用自定义键盘,或者通过 IPC 请求主屏输入法转发。
3.6 实战建议:多屏架构设计
说了这么多,我总结一下个人习惯的多屏架构设计原则:
- 屏幕隔离: 每个屏幕有独立的 Window 栈和 Task 栈
- 通信统一: 使用
LocalBroadcastManager或LiveData进行跨屏通信 - 焦点仲裁: 设计一个焦点仲裁服务,统一管理多屏焦点请求
- 生命周期同步: Presentation 的生命周期跟随宿主 Activity,但要有独立的保存状态机制
最后说一句: 多屏开发没有银弹。每个车厂的屏幕布局、分辨率、交互逻辑都不一样。但 Presentation 类、Activity 启动模式、跨屏拖拽和焦点管理,这四个点你吃透了,就能应对 80% 的场景。剩下的 20%,靠调试和踩坑来补。