8、输入系统:车载输入设备(旋钮、触摸板、方向盘按键)、InputReader 与 InputDispatcher 定制

车载输入系统,说白了就是车机怎么「听懂」用户的物理操作。手机只有触摸屏,但车里还有旋钮、触摸板、方向盘按键。这些设备怎么接入 Android?InputReader 和 InputDispatcher 又该怎么改?

我个人习惯把车载输入分成两类:一类是「触摸替代型」,比如触摸板;另一类是「事件触发型」,比如旋钮和方向盘按键。这两类的处理逻辑完全不同。

8.1 车载输入设备的硬件抽象

Android 的输入系统,底层依赖 Linux 的 input 子系统。每个物理设备都会注册为一个 /dev/input/eventX 节点。车机里常见的设备类型有:

设备类型 Linux 事件类型 典型用途
旋钮编码器 EV_REL / EV_ABS 音量调节、菜单滚动
触摸板 EV_ABS (MT协议) 光标移动、手势操作
方向盘按键 EV_KEY 接听电话、切换歌曲
硬按键 EV_KEY Home、Back、音量

嗯,这里要注意:旋钮和触摸板在驱动层上报的是原始数据,比如旋钮的「顺时针旋转 1 格」对应一个 REL_DIAL 事件。但 Android 上层需要的是「滚动」或「点击」语义。所以中间必须有一层转换。

核心原则:车载输入设备在驱动层只上报「物理动作」,语义转换由 InputReader 或 InputDispatcher 完成。

8.2 InputReader 定制:从原始事件到输入事件

InputReader 是 Android 输入系统的第一站。它从 /dev/input 读取原始事件,然后根据设备类型和配置,转换成 InputEvent

我曾经在项目中遇到一个问题:旋钮的旋转事件被当成了鼠标滚轮事件,导致 UI 上的滚动方向反了。后来发现是 .idc 文件配置不对。

定制 InputReader 通常有两种方式:

  1. 修改 .idc 配置文件:在 /system/usr/idc/ 下放置设备对应的配置文件,指定设备类型、按键映射等。
  2. 修改 InputReader 源码:在 frameworks/native/services/inputflinger/reader/ 中,添加自定义的 InputMapper

举个例子,旋钮的 .idc 配置:

# /system/usr/idc/Vendor_1234_Product_5678.idc
device.internal = 1
touch.deviceType = touchPad
cursor.mode = navigation
cursor.orientation = 90

但如果你需要更复杂的逻辑,比如旋钮的「长按」触发特殊功能,那就得写自定义 Mapper 了。

我的建议:能用 .idc 解决的,别动源码。我见过太多团队因为改 InputReader 导致 CTS 测试挂掉。配置文件是官方推荐的方式。

8.3 InputDispatcher 定制:事件分发策略

InputDispatcher 负责把输入事件分发给目标窗口。车载场景下,有几个特殊需求:

  • 方向盘按键的全局响应:无论当前焦点在哪个应用,音量键、接听键必须立即响应。
  • 旋钮的焦点控制:旋钮滚动时,焦点应该跟随高亮控件移动,而不是像鼠标那样自由移动。
  • 触摸板的区域映射:触摸板可能只映射到屏幕的某个区域,比如只控制中控屏,不影响仪表盘。

定制 InputDispatcher 的核心是修改 InputDispatcher::dispatchOnce()findFocusedWindowTargets() 方法。

我记得有一次,客户要求方向盘上的「上一曲/下一曲」按键,在音乐应用没打开时也能切换蓝牙音频。这其实就是在 InputDispatcher 里加了一个「全局按键监听」的逻辑。

// 伪代码:在 InputDispatcher 中拦截方向盘按键
status_t InputDispatcher::dispatchKeyLocked(
    const sp<KeyEntry>& entry, ...) {
    // 如果是方向盘媒体键
    if (entry->keyCode == AKEYCODE_MEDIA_PLAY_PAUSE) {
        // 直接发送给系统服务,不经过焦点窗口
        mPolicy->interceptKeyBeforeDispatching(entry);
        return OK;
    }
    // 其他按键走正常分发流程
    return dispatchKeyToFocusedWindow(entry, ...);
}

注意:修改 InputDispatcher 会影响所有输入事件的分发性能。我曾经见过一个团队在 dispatchOnce 里加了日志打印,结果导致触摸延迟增加了 30ms。线上环境一定要做性能测试。

8.4 旋钮与触摸板的特殊处理

旋钮和触摸板是车载特有的输入设备。它们的处理逻辑和手机触摸屏完全不同。

旋钮的处理要点:

  • 旋钮的「旋转」事件应该映射为 KEYCODE_DPAD_UP/DOWNKEYCODE_DPAD_LEFT/RIGHT
  • 旋钮的「按下」事件映射为 KEYCODE_DPAD_CENTERKEYCODE_ENTER
  • 旋钮的「长按」可以映射为 KEYCODE_BACK 或自定义按键。

触摸板的处理要点:

  • 触摸板通常使用相对坐标模式(REL_X, REL_Y),而不是绝对坐标。
  • 触摸板的点击映射为 BTN_LEFTBTN_TOUCH
  • 触摸板的手势(双指缩放、滑动)需要额外的 Gesture Detector 支持。

我个人习惯在 InputReader 层把触摸板的原始事件转换成 MotionEvent,然后在 InputDispatcher 层根据当前 UI 布局,决定这个事件是控制光标还是控制焦点。

8.5 实战:方向盘按键的全局响应

这是车载开发中最常见的需求。方向盘按键必须「抢断」当前应用的焦点,直接响应。

实现方案有两种:

  1. 在 InputDispatcher 中拦截:如上文代码所示,在 dispatchKeyLocked 中判断按键码,直接调用系统服务。
  2. 在 SystemUI 中注册全局按键监听:通过 WindowManagerService 注册一个全局的 InputMonitor

我推荐第二种方案,因为它不需要修改系统框架层代码,维护成本低。

// SystemUI 中注册全局按键监听
InputMonitor monitor = mWindowManagerService.createInputMonitor(
    "SteeringWheel", Display.DEFAULT_DISPLAY);
InputChannel inputChannel = monitor.getInputChannel();
InputHandler handler = new InputHandler(inputChannel) {
    @Override
    public void handleKey(KeyEvent event) {
        if (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
            // 直接处理,不交给焦点窗口
            mMediaService.togglePlayPause();
            event.setHandled(true);
        }
    }
};

避坑指南:我曾经在 InputDispatcher 里直接拦截按键,结果导致 CarService 里的按键监听收不到事件。后来发现是拦截顺序的问题。建议优先使用 InputMonitor,它不会破坏原有的分发链路。

8.6 知识体系总览

下面这张图总结了车载输入系统的核心流程和定制点:

车载输入系统核心流程 物理设备 旋钮 / 触摸板 / 方向盘按键 Linux Input 子系统 /dev/input/eventX EventHub 读取原始事件 .idc 配置 设备映射 InputReader 定制点:自定义 InputMapper / 修改 .idc / 事件转换 InputDispatcher 定制点:全局按键拦截 / 焦点策略 / 区域映射 目标窗口 / SystemUI / CarService 最终响应:UI 更新 / 媒体控制 / 导航操作 定制区域 InputReader InputDispatcher .idc 配置 InputMonitor 全局拦截

从这张图可以看出,车载输入系统的定制主要集中在 InputReader 和 InputDispatcher 两层。下层(物理设备、Linux 驱动)一般不动,上层(应用层)通过 InputMonitor 或焦点策略来配合。

嗯,最后说一句:车载输入系统的定制,核心是「理解物理设备的语义,然后映射到 Android 的输入模型」。不要试图绕过 Android 的输入框架,那只会让你陷入无尽的兼容性坑里。


公众号:蓝海资料掘金营,微信deep3321