ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

8、键盘事件Mapper:KeyboardInputMapper的process()函数,键码映射与MetaState处理

8、键盘事件Mapper:KeyboardInputMapper的process()函数,键码映射与MetaState处理 8.1 process()函数的整体流程先看函数签名。它接收一个NotifyKeyArgs参数里面封装了原始按键信息。返回值是nsecs_t表示下一次按键重复的延迟时间。nsecs_t KeyboardInputMapper::process(const NotifyKeyArgs* args) { // 1. 获取按键动作按下、抬起、重复 int32_t action args-action; // 2. 获取原始扫描码 int32_t scanCode args-scanCode; // 3. 获取当前时间戳 nsecs_t eventTime args-eventTime; // ... 后续处理 }我个人习惯把process()分成三个阶段来看预处理阶段获取原始数据检查设备状态核心处理阶段键码映射、MetaState计算、按键重复判断后处理阶段构造KeyEvent分发到上层嗯这里要注意process()并不是每次调用都会产生一个KeyEvent。比如当设备处于休眠状态时按键事件会被直接丢弃。我在项目中就遇到过这种情况——外接键盘在锁屏后按了没反应排查了半天才发现是设备状态检查给过滤掉了。8.2 键码映射从扫描码到Android键码键码映射是process()的核心任务之一。原始硬件上报的是扫描码scan code但上层应用需要的是Android定义的键码key code。这中间需要一个映射表。Android使用.kl文件Key Layout File来定义映射关系。比如key 1 ESCAPE key 2 1 key 3 2 key 4 3 key 5 4 key 6 5 key 7 6 key 8 7 key 9 8 key 10 9 key 11 0 key 12 MINUS key 13 EQUALS左边是扫描码右边是Android键码。映射过程在mapKey()函数中完成void KeyboardInputMapper::mapKey(int32_t scanCode, int32_t usageCode, int32_t* outKeyCode, uint32_t* outFlags) { // 从KeyCharacterMap中查找映射 const KeyCharacterMap::KeyEntry* keyEntry mContext-getKeyCharacterMap()-getKey(scanCode); if (keyEntry) { *outKeyCode keyEntry-keyCode; *outFlags keyEntry-flags; } else { // 找不到映射使用默认值 *outKeyCode AKEYCODE_UNKNOWN; *outFlags 0; } }这里有个坑——usageCode是HID设备用的而scanCode是传统键盘用的。两者不能混用。我曾经帮一个同事排查问题他的外接键盘某些键没反应最后发现是.kl文件里把usageCode和scanCode搞反了。注意事项键码映射失败时outKeyCode会被设为AKEYCODE_UNKNOWN。这种情况下上层应用收到的按键事件键码是0通常会被忽略。如果你的键盘某个键按了没反应先检查.kl文件映射是否正确。8.3 MetaState处理Shift、Ctrl、Alt的位运算艺术MetaState说白了就是修饰键的状态。Shift、Ctrl、Alt、Meta这些键按下去后会影响其他按键的行为。Android用32位整数来表示这些状态每一位代表一个修饰键。来看看MetaState的定义常量值说明AMETA_SHIFT_ON0x01Shift键按下AMETA_CTRL_ON0x02Ctrl键按下AMETA_ALT_ON0x04Alt键按下AMETA_META_ON0x08Meta键Win/Command按下AMETA_CAPS_LOCK_ON0x10Caps Lock锁定AMETA_NUM_LOCK_ON0x20Num Lock锁定process()中处理MetaState的核心逻辑在updateMetaState()函数里void KeyboardInputMapper::updateMetaState(int32_t keyCode, int32_t action) { int32_t metaState mMetaState; switch (keyCode) { case AKEYCODE_SHIFT_LEFT: case AKEYCODE_SHIFT_RIGHT: if (action AKEY_EVENT_ACTION_DOWN) { metaState | AMETA_SHIFT_ON; } else if (action AKEY_EVENT_ACTION_UP) { metaState ~AMETA_SHIFT_ON; } break; case AKEYCODE_CTRL_LEFT: case AKEYCODE_CTRL_RIGHT: if (action AKEY_EVENT_ACTION_DOWN) { metaState | AMETA_CTRL_ON; } else if (action AKEY_EVENT_ACTION_UP) { metaState ~AMETA_CTRL_ON; } break; case AKEYCODE_ALT_LEFT: case AKEYCODE_ALT_RIGHT: // 类似处理 break; case AKEYCODE_CAPS_LOCK: if (action AKEY_EVENT_ACTION_DOWN) { // 切换Caps Lock状态 metaState ^ AMETA_CAPS_LOCK_ON; } break; } mMetaState metaState; }你看这里用了位运算的经典操作|用于设置某一位按下时 ~用于清除某一位抬起时^用于切换某一位Caps Lock这种开关型说实话位运算在嵌入式系统里太常见了。我刚开始做Android系统时看到这种代码总觉得不如用结构体清晰。但后来发现用位运算做状态管理性能好、内存省而且方便做组合判断。比如判断ShiftCtrlA这种组合键直接(metaState (AMETA_SHIFT_ON | AMETA_CTRL_ON))就搞定了。小技巧调试MetaState问题时我习惯在updateMetaState()里加个日志打印每次变化后的mMetaState值。用__android_log_print(ANDROID_LOG_DEBUG, Input, metaState0x%08x, mMetaState)一眼就能看出哪个位不对。8.4 按键重复处理键盘按住不放时系统会持续产生重复按键事件。这个逻辑也在process()里处理。关键代码在repeatKey()函数中nsecs_t KeyboardInputMapper::repeatKey() { if (mKeyDownScanner ! -1) { // 计算下一次重复的延迟时间 nsecs_t nextRepeat systemTime(SYSTEM_TIME_MONOTONIC) mContext-getKeyRepeatDelay(); // 发送重复按键事件 NotifyKeyArgs args(mContext-getNextId(), ...); mContext-getPolicy()-notifyKey(args); return nextRepeat; } return -1; }这里有两个时间参数很重要初始延迟KeyRepeatTimeout按住后多久开始重复默认500ms重复间隔KeyRepeatDelay每次重复之间的间隔默认50ms这两个值可以在系统设置里调整。我记得有一次用户反馈说键盘重复太慢查了半天发现是某个第三方输入法把重复间隔改成了200ms。嗯这种问题最坑了因为不是系统bug是配置被改了。8.5 构造KeyEvent并分发经过前面的处理最后一步是构造KeyEvent对象并分发到上层。这部分在processKey()函数中完成void KeyboardInputMapper::processKey(nsecs_t eventTime, int32_t action, int32_t keyCode, int32_t scanCode, int32_t metaState, int32_t repeatCount, int32_t flags) { // 构造KeyEvent KeyEvent event; event.initialize(eventTime, action, keyCode, scanCode, metaState, repeatCount, flags, mContext-getDeviceId(), ...); // 分发到InputDispatcher mContext-getDispatcher()-notifyKey(event); }这里要注意flags参数它包含了一些额外信息比如FLAG_FROM_SYSTEM系统按键如音量键、电源键FLAG_VIRTUAL虚拟键盘事件FLAG_LONG_PRESS长按事件这些flag会影响上层如何处理这个按键事件。比如系统按键会优先被PhoneWindowManager拦截不会传到普通应用。8.6 避坑指南我踩过的那些坑讲了这么多最后分享几个我实际工作中遇到的坑MetaState不同步有一次外接键盘的Shift键按下后弹不起来导致后续所有按键都变成大写。排查发现是键盘硬件问题但系统层面也可以加个超时机制如果Shift按下超过5秒没抬起自动重置状态。键码映射冲突多个.kl文件同时存在时映射规则会叠加。我曾经遇到一个设备插上不同的键盘会加载不同的.kl文件结果某个键在键盘A上是Enter在键盘B上变成了Backspace。解决方案是在.kl文件里加device限定符。重复按键的边界情况快速按下抬起时重复计数器可能会乱。我记得有个bug是快速按两下空格结果系统识别成了长按。后来发现是repeatCount的计算逻辑在快速操作时没重置。好了关于KeyboardInputMapper::process()的核心内容就这些。说白了键盘事件的处理就是三个步骤映射键码、管理修饰键状态、处理重复逻辑。掌握了这些你就能理解为什么有些按键组合在某些应用里不生效也能自己排查键盘相关的问题了。
返回列表