车机跨屏交互实战:Android Automotive 跨屏显示与触摸传递实践

文章目录

    • [1. 用 `mirrorDisplay()` 做镜像显示](#1. 用 mirrorDisplay() 做镜像显示)
      • [1.1 基本思路](#1.1 基本思路)
      • [1.2 `mirrorDisplay` 的接口与调用方式](#1.2 mirrorDisplay 的接口与调用方式)
        • [1.2.1 `IWindowManager` 接口定义](#1.2.1 IWindowManager 接口定义)
        • [1.2.2 `SurfaceControl` 与 `Transaction`](#1.2.2 SurfaceControlTransaction)
        • [1.2.3 典型调用示例](#1.2.3 典型调用示例)
      • [1.3 它为什么延迟低](#1.3 它为什么延迟低)
      • [1.4 Native 侧实现原理](#1.4 Native 侧实现原理)
      • [1.5 坐标系与显示投影](#1.5 坐标系与显示投影)
      • [1.6 性能对比](#1.6 性能对比)
    • [2. 用 `injectMotionEvent` 做触摸传递](#2. 用 injectMotionEvent 做触摸传递)
      • [2.1 为什么要显式做输入注入](#2.1 为什么要显式做输入注入)
      • [2.2 坐标映射](#2.2 坐标映射)
      • [2.3 注入流程](#2.3 注入流程)
      • [2.4 为什么这种方式更稳](#2.4 为什么这种方式更稳)
    • [3. `SurfaceControl` 父节点差异:一个容易混淆的补充点](#3. SurfaceControl 父节点差异:一个容易混淆的补充点)
    • [4. 为什么透明覆盖层和 blocker 往往不可靠](#4. 为什么透明覆盖层和 blocker 往往不可靠)
    • [5. 权限要求与落地前提](#5. 权限要求与落地前提)
      • [5.1 常见权限](#5.1 常见权限)
      • [5.2 系统签名要求](#5.2 系统签名要求)
    • [6. 软件流程图](#6. 软件流程图)
    • [7. 总结](#7. 总结)
    • [8. 参考资料](#8. 参考资料)

本文首发地址 https://h89.cn/archives/498.html

在 Android Automotive 场景中,实现跨屏镜像与触摸控制,比较直接的一条路径是:

  1. mirrorDisplay() 负责镜像显示
  2. injectMotionEvent 负责触摸传递

这样做的好处是职责清晰:

  • 显示问题交给 SurfaceControl
  • 输入问题交给事件注入

很多实现一开始会把"镜像显示成功"和"触摸自然可用"当成一件事,后面才发现这其实是两条链路。本文按这个主线展开,先讲镜像显示,再讲触摸传递,最后补充一个容易混淆但不应喧宾夺主的问题:SurfaceControl 父节点不同,确实可能带来不同的触摸结果。


1. 用 mirrorDisplay() 做镜像显示

1.1 基本思路

Android 提供了系统级的镜像能力,可以通过 IWindowManager.mirrorDisplay() 把某个显示屏的内容镜像为一个 SurfaceControl。随后,再通过 SurfaceControl.Transaction 去控制:

  • 父子层级
  • 显示区域
  • 裁剪区域
  • 缩放与矩阵
  • 图层顺序

这条链路的优势是延迟低、路径短,不需要走截图轮询或编解码。

1.2 mirrorDisplay 的接口与调用方式

1.2.1 IWindowManager 接口定义
java 复制代码
// IWindowManager.java
public interface IWindowManager extends IInterface {
    boolean mirrorDisplay(int displayId, SurfaceControl outSurfaceControl) throws RemoteException;

    SurfaceControl mirrorWallpaperSurface(int displayId) throws RemoteException;
}
1.2.2 SurfaceControlTransaction
java 复制代码
// SurfaceControl.java
public final class SurfaceControl implements Parcelable {
    public long mNativeObject;

    public static SurfaceControl mirrorSurface(SurfaceControl mirrorOf);
}

// SurfaceControl.Transaction
public static class Transaction implements Closeable, Parcelable {
    public long mNativeObject;

    public Transaction show(SurfaceControl sc);
    public Transaction hide(SurfaceControl sc);
    public Transaction setGeometry(SurfaceControl sc, Rect sourceCrop, Rect destFrame, int orientation);
    public Transaction setLayer(SurfaceControl sc, int z);
    public Transaction reparent(SurfaceControl sc, SurfaceControl newParent);
    public Transaction setMatrix(SurfaceControl sc, float dsdx, float dtdx, float dtdy, float dsdy);
    public void apply();
}
1.2.3 典型调用示例
kotlin 复制代码
val windowManager = IWindowManager.Stub.asInterface(
    ServiceManager.getService(Context.WINDOW_SERVICE)
)

val mirrorSc = SurfaceControl.Builder()
    .setName("MirrorDisplay")
    .setBufferSize(1, 1)
    .build()

val success = windowManager.mirrorDisplay(targetDisplayId, mirrorSc)

SurfaceControl.Transaction().apply {
    reparent(mirrorSc, parentSc)
    setGeometry(
        mirrorSc,
        Rect(0, 0, sourceWidth, sourceHeight),
        Rect(destLeft, destTop, destRight, destBottom),
        Surface.ROTATION_0
    )
    show(mirrorSc)
    apply()
}

1.3 它为什么延迟低

因为这里处理的是系统合成层,不是视频流。

更准确地说:

  1. mirrorDisplay() 创建的是 mirror/clone layer
  2. 该 layer 引用目标显示对应的 LayerStack
  3. SurfaceFlinger 在合成时直接把这些图层投影到目标区域

因此,它更接近"在系统合成树中挂一层引用",而不是"复制一张画面"。

1.4 Native 侧实现原理

以 Android 14 为例,mirrorDisplay() 大致会经过以下链路:

  1. IWindowManager.mirrorDisplay()
  2. JNI 层 android_view_SurfaceControl.cpp
  3. Native 客户端 SurfaceComposerClient
  4. 服务端 SurfaceFlinger::mirrorDisplay

SurfaceFlinger 侧,简化后的逻辑可以表示为:

cpp 复制代码
// SurfaceFlinger.cpp (简化逻辑)
status_t SurfaceFlinger::mirrorDisplay(DisplayId displayId,
                                       const LayerCreationArgs& args,
                                       gui::CreateSurfaceResult& outResult) {
    const auto display = getDisplayDeviceLocked(displayId);
    ui::LayerStack layerStack = display->getLayerStack();

    LayerCreationArgs mirrorArgs = LayerCreationArgs::fromOtherArgs(args);
    mirrorArgs.flags |= ISurfaceComposerClient::eNoColorFill;
    mirrorArgs.addToRoot = true;
    mirrorArgs.layerStackToMirror = layerStack;

    sp<Layer> rootMirrorLayer;
    createEffectLayer(mirrorArgs, &outResult.handle, &rootMirrorLayer);
    addClientLayer(mirrorArgs, ...);

    return NO_ERROR;
}

这里的关键点是:镜像本质上是一层特殊的 EffectLayer,它不是普通 View,而是系统 layer tree 中的一层。

1.5 坐标系与显示投影

镜像显示不只是"把内容放出来",还要处理坐标系问题。

常见差异来自两类坐标:

  1. LayerStack 逻辑坐标系
  2. DisplayDevice 物理投影坐标系

如果源显示存在旋转、视口裁剪或投影修正,镜像层可能不会自动继承这些变换。此时通常需要结合:

  • DisplayInfo
  • rotation
  • 逻辑尺寸
  • 目标显示区域

再通过 setGeometry()setMatrix() 做修正。

1.6 性能对比

方案 延迟 CPU 占用 内存占用 适用场景
SurfaceControl 镜像 极低 极低 共享 Layer/Buffer 多屏镜像、车机显示
MediaProjection + 编码 50-100ms 独立 Buffer 录屏、远程控制
OpenGL 拷贝渲染 1-2 帧 双份资源 特效处理
截图轮询 100ms+ 独立 Buffer 静态快照

2. 用 injectMotionEvent 做触摸传递

2.1 为什么要显式做输入注入

mirrorDisplay() 解决的是显示问题,不天然保证触摸就会落到源显示。

因此,如果目标是稳定地把镜像区域的点击、滑动、多指等操作传回源 display,更可控的方式通常是:

  1. 监听镜像宿主上的触摸
  2. 把触摸坐标映射回源显示坐标
  3. 通过 injectMotionEvent 注入到目标 display

2.2 坐标映射

当用户触摸镜像区域时,需要把局部坐标映射回源显示坐标:

text 复制代码
targetX = (touchX - mirrorDestLeft) / mirrorDestWidth * sourceDisplayWidth
targetY = (touchY - mirrorDestTop) / mirrorDestHeight * sourceDisplayHeight

其中:

  • mirrorDestLeft/Top/Width/Height:镜像区域在宿主中的位置和尺寸
  • sourceDisplayWidth/Height:源显示的原始分辨率

2.3 注入流程

典型流程如下:

  1. 监听宿主层触摸
  2. 判断是否命中镜像区域
  3. 做坐标映射
  4. 构建 MotionEvent
  5. 设置目标 displayId
  6. 通过 InputManager.injectInputEvent() 注入

从职责划分看,这条链路非常清晰:

  • mirrorDisplay() 负责"看见"
  • injectMotionEvent 负责"能操作"

2.4 为什么这种方式更稳

因为它不依赖设备是否存在"自动触摸传递"的特殊行为。

相比之下,显式注入的优点是:

  • 行为边界明确
  • 可做精确坐标控制
  • 更适合产品化方案

代价则是:

  • 实现复杂度更高
  • 依赖系统权限

3. SurfaceControl 父节点差异:一个容易混淆的补充点

虽然本文主线是"显示靠 mirrorDisplay,触摸靠 injectMotionEvent",但有一个容易让人误判的问题仍值得单独提一下:mirror layer reparent 到不同父节点时,可能出现不同的触摸结果。

常见的两类父节点是:

  1. View Root SurfaceControl
  2. SurfaceView SurfaceControl

它们的差异不只是"显示挂载位置不同",而是:

  • mirror layer 所在的 layer tree 位置不同
  • 在窗口中的层级关系不同
  • 被输入系统命中的方式也可能不同

在一些设备上,会观察到这样的现象:

  • 挂到 View Root SurfaceControl 时,更容易出现全局输入影响,甚至出现自动把触摸传递到源 display
  • 挂到 SurfaceView SurfaceControl 时,这种现象会消失

因此,如果你在排查"为什么有时会自动透传、有时不会"时,reparent 的父节点应该被视为一个重要变量。但从工程实现角度,它更适合作为补充分析,而不是替代 injectMotionEvent 这条稳定输入链路。


4. 为什么透明覆盖层和 blocker 往往不可靠

很多人在第一次遇到这类问题时,会优先想到:

  • 在镜像上面叠一个透明 View
  • 加一个 blocker / input sink
  • 设置某些输入丢弃标志

这些思路看起来合理,但常常不稳定。根本原因是:

  1. 命中的对象不是普通 XML 叠层
  2. 真正起作用的是系统最终生成的输入窗口和 layer tree
  3. mirror layer 的命中位置,取决于它被挂在什么父节点下,以及系统如何生成输入窗口信息

因此,应用层的"看起来在上面",不等于输入链路中的"实际先命中它"。


5. 权限要求与落地前提

仅有正确的显示与输入逻辑还不够,这类方案通常还依赖系统权限。

5.1 常见权限

xml 复制代码
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.INJECT_EVENTS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />

5.2 系统签名要求

很多这类能力不仅需要声明权限,还需要系统签名或特权应用身份。

尤其是:

  • INJECT_EVENTS
  • INTERACT_ACROSS_USERS_FULL

如果忽略这一点,很容易让读者误以为只要逻辑正确,这套方案就能在普通三方应用里直接落地。


6. 软件流程图

text 复制代码
┌─────────────────────────────────────────────────────────────┐
│                 Host Window / Fragment / SurfaceView         │
├─────────────────────────────────────────────────────────────┤
│ 1. Surface 生命周期就绪                                      │
│    ↓                                                        │
│ 2. 创建镜像                                                 │
│    - 创建空 SurfaceControl                                  │
│    - 调用 mirrorDisplay()                                   │
│    - reparent 到目标父 SurfaceControl                       │
│    - 设置 geometry / matrix                                 │
│    ↓                                                        │
│ 3. 镜像区域显示                                             │
│    - 等比例缩放                                             │
│    - 坐标系修正                                             │
│    ↓                                                        │
│ 4. 触摸处理                                                 │
│    - 监听宿主触摸                                           │
│    - 坐标映射                                               │
│    - injectMotionEvent                                      │
│    ↓                                                        │
│ 5. InputDispatcher / 目标 Display                           │
└─────────────────────────────────────────────────────────────┘

7. 总结

如果把这套方案浓缩成一句话,就是:

mirrorDisplay() 解决镜像显示,用 injectMotionEvent 解决触摸传递。

更完整一点的工程结论是:

  1. mirrorDisplay() 适合做低延迟镜像显示
  2. injectMotionEvent 适合做可控的触摸传递
  3. 两者职责分离后,整条链路更稳定、更容易排查问题
  4. SurfaceControl 父节点差异会影响某些设备上的触摸表现,但更适合作为补充分析,而不是依赖它做正式输入方案

8. 参考资料

相关推荐
luanma1509803 小时前
PHP vs Java:Web开发终极对决
android
草莓熊Lotso3 小时前
Linux 进程信号深度解析(上):信号的产生与本质(含完整案例)
android·linux·运维·服务器·数据库·c++·mysql
鹏程十八少3 小时前
5.Android 如何用腾讯Shadow在双11电商场景的完整复盘(实战2年),实现热修复(全网最详细实战案例)
android·前端·面试
0pen13 小时前
Phone Control - 高效的 Android 设备群控解决方案
android·爬虫·ai编程
XiaoLeisj3 小时前
Android 媒体能力实战:从 Media3 音视频播放到 CameraX 拍照与视频录制
android·音视频·媒体·android jetpack
AI创界者3 小时前
Gemini/Grok/ChatGPT 安卓版安装教程:手机 AI 助手快速上手指南
android·chatgpt·智能手机
gregmankiw14 小时前
Nemotron架构(Mamba3+Transformer+Moe)
android·深度学习·transformer
xianjian091216 小时前
MySQL 的 INSERT(插入数据)详解
android·数据库·mysql