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 通常有两种方式:
- 修改 .idc 配置文件:在
/system/usr/idc/下放置设备对应的配置文件,指定设备类型、按键映射等。 - 修改 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/DOWN或KEYCODE_DPAD_LEFT/RIGHT。 - 旋钮的「按下」事件映射为
KEYCODE_DPAD_CENTER或KEYCODE_ENTER。 - 旋钮的「长按」可以映射为
KEYCODE_BACK或自定义按键。
触摸板的处理要点:
- 触摸板通常使用相对坐标模式(REL_X, REL_Y),而不是绝对坐标。
- 触摸板的点击映射为
BTN_LEFT或BTN_TOUCH。 - 触摸板的手势(双指缩放、滑动)需要额外的 Gesture Detector 支持。
我个人习惯在 InputReader 层把触摸板的原始事件转换成 MotionEvent,然后在 InputDispatcher 层根据当前 UI 布局,决定这个事件是控制光标还是控制焦点。
8.5 实战:方向盘按键的全局响应
这是车载开发中最常见的需求。方向盘按键必须「抢断」当前应用的焦点,直接响应。
实现方案有两种:
- 在 InputDispatcher 中拦截:如上文代码所示,在 dispatchKeyLocked 中判断按键码,直接调用系统服务。
- 在 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 知识体系总览
下面这张图总结了车载输入系统的核心流程和定制点:
从这张图可以看出,车载输入系统的定制主要集中在 InputReader 和 InputDispatcher 两层。下层(物理设备、Linux 驱动)一般不动,上层(应用层)通过 InputMonitor 或焦点策略来配合。
嗯,最后说一句:车载输入系统的定制,核心是「理解物理设备的语义,然后映射到 Android 的输入模型」。不要试图绕过 Android 的输入框架,那只会让你陷入无尽的兼容性坑里。
公众号:蓝海资料掘金营,微信deep3321