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_CALL 和 KEYCODE_ENDCALL。所以映射的时候,一定要确认目标 keycode 在 Automotive 的 KeyEvent 类里是存在的。
getevent 命令抓一下原始 scancode,再写映射。
8.2 旋钮:增量事件的特殊处理
旋钮和按键不一样。按键是“按下-抬起”的离散事件,旋钮是“顺时针-逆时针”的连续增量事件。Android 原生把旋钮事件当作 REL_WHEEL 或 REL_HWHEEL 来处理。但车机上的旋钮,往往还带一个“按下”功能(比如按下旋钮确认选择)。
我个人的做法是,把旋钮的旋转事件映射成 KEYCODE_DPAD_UP 和 KEYCODE_DPAD_DOWN,或者 KEYCODE_DPAD_LEFT 和 KEYCODE_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 之间。你可以在设置里加一个“触摸板灵敏度”的选项,让用户自己调。
MotionEvent 的 ACTION_MOVE 事件,并设置 SOURCE_TOUCHPAD 标志。这样应用层才能正确识别来源。
8.4 事件分发:InputDispatcher 的定制
事件映射完了,怎么分发给正确的窗口?Android 的 InputDispatcher 默认是按焦点窗口分发的。但车机上有个特殊场景:方向盘按键可能同时需要被多个组件响应。比如,按一下“音量+”,音乐应用要调大音量,同时 HUD 上要显示音量变化的动画。
这时候,单靠焦点分发就不够了。我建议在 InputDispatcher 里加一个“广播”机制。具体做法是:
- 在
InputDispatcher的notifyKey方法里,判断 keycode 是否属于“广播按键”(比如音量键、静音键)。 - 如果是,除了分发给焦点窗口,还额外分发给注册了“全局按键监听”的服务。
- 这些服务通过
WindowManager的addKeyListener接口注册。
代码大概长这样:
// 在 InputDispatcher.cpp 中
void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
// ... 原有逻辑 ...
// 检查是否是广播按键
if (isBroadcastKey(args->keyCode)) {
// 额外分发给全局监听器
dispatchToGlobalListeners(args);
}
// ... 继续原有分发 ...
}
嗯,这里要注意。广播按键的分发不能阻塞主流程。我建议用异步方式,或者把全局监听器的处理放到一个单独的线程里。否则,一个监听器卡住了,整个输入系统都会受影响。
8.5 知识体系总览
说了这么多,我画了一张图,把输入系统定制的核心逻辑串起来。你可以对照着看:
这张图从左到右,从上到下,就是输入事件从物理设备到应用层的完整路径。你定制的时候,每一层都可能需要动。但记住一个原则:能不改框架层,就不改框架层。尽量在 .kl 文件和 HAL 层解决问题。实在不行,再动 InputDispatcher。
- 方向盘按键:核心是 .kl 文件的 scancode 到 keycode 映射。用
getevent抓原始码。 - 旋钮:把增量事件转成方向键事件。注意消抖和限速。
- 触摸板:用相对映射 + 加速度曲线。可调灵敏度。
- 事件分发:广播按键需要额外处理。用全局监听器,但注意异步。
好了,这一章就到这里。下一章我们会聊音频系统的定制,那又是一个大坑。到时候见。
公众号:蓝海资料掘金营,微信deep3321