车机跨屏交互实战: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. 参考资料

相关推荐
Kapaseker10 小时前
MVVM 旧城改造,边界划分各有招
android·kotlin
我滴老baby10 小时前
多智能体协作系统设计当AI学会团队合作效率翻十倍
android·开发语言·人工智能
StockTV10 小时前
新加坡股票API 实时行情、K 线及指数数据
android·java·spring boot·后端·区块链
草莓熊Lotso11 小时前
LangChain从入门到精通:环境搭建→核心能力→LCEL链式编程全实战
android·java·linux·服务器·langchain
私人珍藏库1 天前
【Android】聆听岛[特殊字符]聚合全网音乐[特殊字符]免费听歌下载神器[特殊字符] 聚合音乐平台|无损母带下载|歌词封面同步|免费无广告听歌工具
android·人工智能·工具·软件·多功能
YF02111 天前
Android触摸机制与自定义 View 实战
android·app
Dabei1 天前
Android TV 焦点处理详解:遥控器与空鼠
android·前端
悠哉清闲1 天前
裁剪SurfaceView
android
常利兵1 天前
Android字体字重设置全攻略:XML黑科技+Kotlin动态实现,告别.ttf臃肿
android·xml·科技
therese_100861 天前
安卓-IPC
android