8、输入系统定制:方向盘按键、旋钮、触摸板的输入事件映射与分发

好,我们进入第八章。说实话,这一章的内容,是我个人在 Automotive 项目里花时间最多的地方之一。为什么?因为输入系统太碎了。方向盘按键、旋钮、触摸板,每种设备的物理特性都不一样,Android 原生的输入框架又主要是为手机触屏设计的。你要让这些设备在车机上跑得顺,就得在映射和分发这两个环节下功夫。

我习惯把输入定制分成三层来看:物理层(按键怎么上报)、映射层(上报的 scancode 转成什么 keycode)、分发层(事件最终落到哪个窗口)。今天我们就一层层拆开讲。

8.1 方向盘按键:从 scancode 到 keycode 的映射

方向盘按键,说白了就是一组 GPIO 或者 CAN 总线上的信号。硬件按下,驱动层上报一个 scancode。Android 的 EventHub 会读到这个 scancode,然后查 .kl 文件(Key Layout File)把它转成 Android 的 keycode。

举个例子,我遇到过一台车,方向盘上的“音量+”按键上报的 scancode 是 0xC2。但 Android 原生不认识这个码。怎么办?写一个 Vendor_XXXX_Product_XXXX.kl 文件:

# 方向盘音量+按键映射
key 0xC2   VOLUME_UP

# 方向盘音量-按键映射
key 0xC3   VOLUME_DOWN

# 方向盘静音按键
key 0xC4   MUTE

这里有个坑。我曾经遇到过一个项目,方向盘上的“接听电话”按键,映射成了 KEY_CALL。但 Android Automotive 里,电话应用并不直接处理这个 keycode。结果就是按了没反应。后来我查了 KeyEvent.java 的源码,发现 Automotive 里电话相关的 keycode 是 KEYCODE_CALLKEYCODE_ENDCALL。所以映射的时候,一定要确认目标 keycode 在 Automotive 的 KeyEvent 类里是存在的。

注意: 不同车厂的方向盘按键 scancode 可能完全不同。不要指望一份 .kl 文件通吃所有车型。建议在开发阶段用 getevent 命令抓一下原始 scancode,再写映射。

8.2 旋钮:增量事件的特殊处理

旋钮和按键不一样。按键是“按下-抬起”的离散事件,旋钮是“顺时针-逆时针”的连续增量事件。Android 原生把旋钮事件当作 REL_WHEELREL_HWHEEL 来处理。但车机上的旋钮,往往还带一个“按下”功能(比如按下旋钮确认选择)。

我个人的做法是,把旋钮的旋转事件映射成 KEYCODE_DPAD_UPKEYCODE_DPAD_DOWN,或者 KEYCODE_DPAD_LEFTKEYCODE_DPAD_RIGHT。为什么?因为车机上的焦点导航,大部分是用方向键实现的。旋钮转一下,相当于按了一次方向键,焦点移动一格。这样复用性最好。

举个例子,我在一个项目中,旋钮顺时针转一下,上报 scancode 0x0A,逆时针转一下,上报 0x0B。我在 .kl 文件里这么写:

# 旋钮顺时针 -> 右方向键
key 0x0A   DPAD_RIGHT

# 旋钮逆时针 -> 左方向键
key 0x0B   DPAD_LEFT

# 旋钮按下 -> 确认键
key 0x0C   ENTER

但这里有个细节。旋钮的旋转速度很快,用户可能连续转好几格。如果每次旋转都产生一个 key down 和 key up 事件,那应用层会收到大量重复事件。我建议在驱动层或者 HAL 层做一下消抖和限速。比如,两次旋转事件之间的间隔小于 50ms,就合并成一次。这样应用层处理起来更平滑。

小技巧: 如果旋钮支持“加速”功能(转得快时跳转幅度大),可以在应用层监听 onKeyMultiple() 方法。这个方法会告诉你按键重复的次数,你可以根据次数决定跳转多少格。

8.3 触摸板:绝对坐标与相对坐标的转换

触摸板在车机上越来越常见。但触摸板有个天然的问题:它是相对定位设备,而 Android 的 UI 是绝对定位的。你手指在触摸板上划一下,屏幕上的光标应该移动多少?这中间需要一个映射算法。

我见过两种做法:

  • 绝对映射:触摸板的物理区域直接映射到屏幕区域。手指在触摸板的左上角,光标就在屏幕的左上角。这种方案适合小尺寸触摸板,但用户需要“盲操”,不太直观。
  • 相对映射:手指滑动产生增量,光标按比例移动。这种方案更接近笔记本电脑的触摸板,用户习惯好,但需要做加速度曲线。

我个人推荐相对映射。为什么?因为车机屏幕大,绝对映射下,用户手指移动 1cm,光标可能已经跨了半个屏幕,很难精确控制。相对映射配合加速度,可以让用户慢速滑动时精细控制,快速滑动时快速跨越。

加速度曲线怎么设计?我分享一个我在项目中用过的公式:

// 输入:delta(触摸板上报的位移量)
// 输出:adjustedDelta(实际光标移动量)
float speed = abs(delta) / timeDelta;
float acceleration = 1.0f;
if (speed > THRESHOLD_FAST) {
    acceleration = 1.5f;  // 快速滑动时加速
} else if (speed < THRESHOLD_SLOW) {
    acceleration = 0.5f;  // 慢速滑动时减速
}
int adjustedDelta = (int)(delta * acceleration * SENSITIVITY);

这里的 SENSITIVITY 是一个可调参数,我一般设为 0.8 到 1.2 之间。你可以在设置里加一个“触摸板灵敏度”的选项,让用户自己调。

核心要点: 触摸板的输入事件,最终要转换成 MotionEventACTION_MOVE 事件,并设置 SOURCE_TOUCHPAD 标志。这样应用层才能正确识别来源。

8.4 事件分发:InputDispatcher 的定制

事件映射完了,怎么分发给正确的窗口?Android 的 InputDispatcher 默认是按焦点窗口分发的。但车机上有个特殊场景:方向盘按键可能同时需要被多个组件响应。比如,按一下“音量+”,音乐应用要调大音量,同时 HUD 上要显示音量变化的动画。

这时候,单靠焦点分发就不够了。我建议在 InputDispatcher 里加一个“广播”机制。具体做法是:

  1. InputDispatchernotifyKey 方法里,判断 keycode 是否属于“广播按键”(比如音量键、静音键)。
  2. 如果是,除了分发给焦点窗口,还额外分发给注册了“全局按键监听”的服务。
  3. 这些服务通过 WindowManageraddKeyListener 接口注册。

代码大概长这样:

// 在 InputDispatcher.cpp 中
void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
    // ... 原有逻辑 ...
    
    // 检查是否是广播按键
    if (isBroadcastKey(args->keyCode)) {
        // 额外分发给全局监听器
        dispatchToGlobalListeners(args);
    }
    
    // ... 继续原有分发 ...
}

嗯,这里要注意。广播按键的分发不能阻塞主流程。我建议用异步方式,或者把全局监听器的处理放到一个单独的线程里。否则,一个监听器卡住了,整个输入系统都会受影响。

8.5 知识体系总览

说了这么多,我画了一张图,把输入系统定制的核心逻辑串起来。你可以对照着看:

输入系统定制核心流程 物理设备 方向盘按键 / 旋钮 / 触摸板 驱动层 GPIO / CAN / I2C EventHub 读取原始 scancode 映射层(.kl 文件 + KeyCharacterMap) scancode → keycode | 增量事件 → 方向键 | 绝对坐标 → 相对坐标 分发层(InputDispatcher) 焦点分发 | 广播分发 | 全局监听器 应用层(onKeyDown / onTouchEvent)

这张图从左到右,从上到下,就是输入事件从物理设备到应用层的完整路径。你定制的时候,每一层都可能需要动。但记住一个原则:能不改框架层,就不改框架层。尽量在 .kl 文件和 HAL 层解决问题。实在不行,再动 InputDispatcher。

总结一下:
  • 方向盘按键:核心是 .kl 文件的 scancode 到 keycode 映射。用 getevent 抓原始码。
  • 旋钮:把增量事件转成方向键事件。注意消抖和限速。
  • 触摸板:用相对映射 + 加速度曲线。可调灵敏度。
  • 事件分发:广播按键需要额外处理。用全局监听器,但注意异步。

好了,这一章就到这里。下一章我们会聊音频系统的定制,那又是一个大坑。到时候见。


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