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_TASKFLAG_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 操作,但需要额外处理。

核心思路是:

  • 源端:启动拖拽时,传入 ClipDataDragShadowBuilder
  • 目标端:监听 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 实战建议:多屏架构设计

说了这么多,我总结一下个人习惯的多屏架构设计原则:

  1. 屏幕隔离: 每个屏幕有独立的 Window 栈和 Task 栈
  2. 通信统一: 使用 LocalBroadcastManagerLiveData 进行跨屏通信
  3. 焦点仲裁: 设计一个焦点仲裁服务,统一管理多屏焦点请求
  4. 生命周期同步: Presentation 的生命周期跟随宿主 Activity,但要有独立的保存状态机制

最后说一句: 多屏开发没有银弹。每个车厂的屏幕布局、分辨率、交互逻辑都不一样。但 Presentation 类、Activity 启动模式、跨屏拖拽和焦点管理,这四个点你吃透了,就能应对 80% 的场景。剩下的 20%,靠调试和踩坑来补。