Input系统之InputReader处理按键事件详解

目录
  • 前言
  • 认识按键事件
  • 处理按键事件
    • 扫描码映射按键码
  • 结束

前言

前面几篇文章已经为 Input 系统的分析打好了基础,现在是时候进行更深入的分析了。

通常,手机是不带键盘的,但是手机上仍然有按键,就是我们经常使用的电源键以及音量键。因此还是有必要分析按键事件的处理流程。

那么,掌握按键事件的处理流程,对我们有什么用处呢?例如,手机上添加了一个功能按键,你知道如何把这个物理按键映射到上层,然后处理这个按键吗?又例如,如果设备是不需要电源键,但是系统默认把某一个按键映射为电源键,那么我们如何使这个按键不成为电源键呢?所有这一切,都与按键事件的处理流程相关。

认识按键事件

很多读者可能还不知道按键事件到底长什么样,通过 adb shell getevent 可以获取输入设备产生的元输入事件,按键事件当然也包括在内。

当按下电源键,可以产生如下的按键事件

/dev/input/event0: 0001 0074 00000001
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0001 0074 00000000
/dev/input/event0: 0000 0000 00000000

/dev/input/event0 是内核为输入设备生成的设备文件,它代表一个输入设备,它后面的数据格式为 type code value

以第一行为例,几个数据的含义如下

  • 0001 代表输入设备产生事件的类型。此时电源键产生的是一个按键类型事件,而如果手指在触摸设备上滑动,产生是一个坐标类型事件。
  • 0074 表示按键的扫描码(code),这个扫描码是与输入设备相关,因此不同的设备上的电源键,产生的扫描码可能不同。
  • 00000001 表示按键值(value)。 00000001 表示按键被按下,00000000 表示按键抬起。

因此,一个输入设备产生的事件,可以通过 type + code + value 的形式表示。

注意,以上这些数据都是元输入事件的数据,是由内核直接为输入设备生成的数据,因此读起来没那么直观。我们可以通过 adb shell getevent -l 显示输入系统对这些数据的分析结果,当按下电源键,会出现如下结果

/dev/input/event0: EV_KEY       KEY_POWER            DOWN     
/dev/input/event0: EV_SYN       SYN_REPORT           00000000 
/dev/input/event0: EV_KEY       KEY_POWER            UP
/dev/input/event0: EV_SYN       SYN_REPORT           00000000

第一行数据,EV_KEY + KEY_POWER + DOWN 表示电源键按下。

第三行数据,EV_KEY + KEY_POWER + UP 表示电源键抬起。

第二行和第四行的数据,EV_SYN + SYN_REPORT 表示之前的一个事件的数据已经发送完毕,需要系统同步之前一个事件的所有数据。对于一个按键,一个事件只有一条数据,然而对于触摸板上的滑动事件,例如手指按下事件,它的数据可不止一条,我们将在后面的文章中看到。

好,既然已经以按键事件的数据有了基本的认识,那么接下来开始着手分析按键事件的处理流程。

处理按键事件

从前面文章可知,InputReader 从 EventHub 中获取了输入事件的数据,然后调用如下函数进行处理

void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
    for (const RawEvent* rawEvent = rawEvents; count;) {
        int32_t type = rawEvent->type;
        size_t batchSize = 1;
        if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
            int32_t deviceId = rawEvent->deviceId;
            // 获取同一个设备的元输入事件的数量
            while (batchSize < count) {
                if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT ||
                    rawEvent[batchSize].deviceId != deviceId) {
                    break;
                }
                batchSize += 1;
            }
            // 批量处理同一个设备的元输入事件
            processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
        } else {
            // ...
        }
        count -= batchSize;
        rawEvent += batchSize;
    }
}
void InputReader::processEventsForDeviceLocked(int32_t eventHubId, const RawEvent* rawEvents,
                                               size_t count) {
    auto deviceIt = mDevices.find(eventHubId);
    if (deviceIt == mDevices.end()) {
        return;
    }
    std::shared_ptr<InputDevice>& device = deviceIt->second;
    // 如果 InputDevice 没有 InputMapper,那么它不能处理事件
    if (device->isIgnored()) {
        return;
    }
    // InputDevice 批量处理元输入事件
    device->process(rawEvents, count);
}

InputReader 首先找到属于同一个设备的多个事件,然后交给 InputDevice 进行批量处理

// frameworks/native/services/inputflinger/reader/InputDevice.cpp
void InputDevice::process(const RawEvent* rawEvents, size_t count) {
    // 虽然 InputReader 把批量的事件交给 InputDevice,但是 InputDevice 还是逐个处理事件
    for (const RawEvent* rawEvent = rawEvents; count != 0; rawEvent++) {
        if (mDropUntilNextSync) {
            if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
                mDropUntilNextSync = false;
            }
        } else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_DROPPED) {
            // EV_SYN + SYN_DROPPED 表明要丢弃后面的事件,直到 EV_SYN + SYN_REPORT
            mDropUntilNextSync = true;
            reset(rawEvent->when);
        } else {
            // 每一个事件交给 InputMapper 处理
            for_each_mapper_in_subdevice(rawEvent->deviceId, [rawEvent](InputMapper& mapper) {
                mapper.process(rawEvent);
            });
        }
        --count;
    }
}

虽然 InputReader 把事件交给 InputDevice 进行批量处理,但是 InputDevice 是逐个把事件交给它的 InputMapper 处理。

对于键盘类型的输入设备,它的 InputMapper 实现类为 KeyboardInputMapper,它对按键事件处理如下

void KeyboardInputMapper::process(const RawEvent* rawEvent) {
    switch (rawEvent->type) {
        // 处理 EV_KEY
        case EV_KEY: {
            int32_t scanCode = rawEvent->code;
            int32_t usageCode = mCurrentHidUsage;
            mCurrentHidUsage = 0;
            if (isKeyboardOrGamepadKey(scanCode)) {
                // 注意第三个参数,value等于0,才表示按键down, 也就是说,value 为1, 表示按键被按下
                processKey(rawEvent->when, rawEvent->readTime, rawEvent->value != 0, scanCode,
                           usageCode);
            }
            break;
        }
        case EV_MSC: {
            // ...
        }
        // 处理 EV_SYN + SYN_REPORT
        case EV_SYN: {
            if (rawEvent->code == SYN_REPORT) {
                mCurrentHidUsage = 0;
            }
        }
    }
}

其中,我们只需要关心类型为 EV_KEY 类型的事件,它表示一个按键事件,它的处理如下

void KeyboardInputMapper::processKey(nsecs_t when, nsecs_t readTime, bool down, int32_t scanCode,
                                     int32_t usageCode) {
    int32_t keyCode;
    int32_t keyMetaState;
    uint32_t policyFlags;
    // 1. 根据键盘配置文件,把 scanCode 转化为 keycode,并获取 flags
    if (getDeviceContext().mapKey(scanCode, usageCode, mMetaState, &keyCode, &keyMetaState,
                                  &policyFlags)) {
        keyCode = AKEYCODE_UNKNOWN;
        keyMetaState = mMetaState;
        policyFlags = 0;
    }
    // 按下
    if (down) {
        // 根据屏幕方向,再次转换 keyCode
        // Rotate key codes according to orientation if needed.
        if (mParameters.orientationAware) {
            keyCode = rotateKeyCode(keyCode, getOrientation());
        }
        // Add key down.
        ssize_t keyDownIndex = findKeyDown(scanCode);
        if (keyDownIndex >= 0) {
            // key repeat, be sure to use same keycode as before in case of rotation
            keyCode = mKeyDowns[keyDownIndex].keyCode;
        } else {
            // key down
            if ((policyFlags & POLICY_FLAG_VIRTUAL) &&
                getContext()->shouldDropVirtualKey(when, keyCode, scanCode)) {
                return;
            }
            if (policyFlags & POLICY_FLAG_GESTURE) {
                // 如果设备通知支持触摸,那么发送一个 ACTION_CANCEL 事件
                getDeviceContext().cancelTouch(when, readTime);
            }
            KeyDown keyDown;
            keyDown.keyCode = keyCode;
            keyDown.scanCode = scanCode;
            // 保存按下的按键
            mKeyDowns.push_back(keyDown);
        }
        mDownTime = when;
    } else { // 抬起按键
        // Remove key down.
        ssize_t keyDownIndex = findKeyDown(scanCode);
        if (keyDownIndex >= 0) {
            // key up, be sure to use same keycode as before in case of rotation
            keyCode = mKeyDowns[keyDownIndex].keyCode;
            // 移除
            mKeyDowns.erase(mKeyDowns.begin() + (size_t)keyDownIndex);
        } else {
            // key was not actually down
            ALOGI("Dropping key up from device %s because the key was not down.  "
                  "keyCode=%d, scanCode=%d",
                  getDeviceName().c_str(), keyCode, scanCode);
            return;
        }
    }
    // 更新meta状态
    if (updateMetaStateIfNeeded(keyCode, down)) {
        // If global meta state changed send it along with the key.
        // If it has not changed then we'll use what keymap gave us,
        // since key replacement logic might temporarily reset a few
        // meta bits for given key.
        keyMetaState = mMetaState;
    }
    nsecs_t downTime = mDownTime;
    // 外部设备的按键按下时,添加唤醒标志位
    if (down && getDeviceContext().isExternal() && !mParameters.doNotWakeByDefault &&
        !isMediaKey(keyCode)) {
        policyFlags |= POLICY_FLAG_WAKE;
    }
    // 设备是否能生成重复按键事件,一般设备都不支持这个功能
    // 而是由系统模拟生成重复按键事件
    if (mParameters.handlesKeyRepeat) {
        policyFlags |= POLICY_FLAG_DISABLE_KEY_REPEAT;
    }
    // 2. 生成 NotifyKeyArgs, 并加到 QueuedInputListener 队列中
    NotifyKeyArgs args(getContext()->getNextId(), when, readTime, getDeviceId(), mSource,
                       getDisplayId(), policyFlags,
                       down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP/*action*/,
                       AKEY_EVENT_FLAG_FROM_SYSTEM/*flags*/, keyCode, scanCode, keyMetaState, downTime);
    getListener()->notifyKey(&args);
}

我现在只关心手机上电源键以及音量键的处理流程,因此这里的处理过程主要分为两步

  • 根据按键配置文件,把扫描码(scan code)转换为按键码(key code),并从配置文件中获取策略标志位(policy flag)。不同的输入设备的同一种功能的按键,例如电源键,产生的扫描码不一定都相同,Android 系统需要把扫描码映射为同一个按键码进行处理。
  • 创建一个事件 NotifyKeyArgs,并加入到 QueuedInputListener 队列中。从前面的文章可知,当 InputReader 处理完从 EventHub 读到的事件后,会刷新这个队列,从而把事件发送给 InputClassifier。而对于按键事件,InputClassifier 不做任何加工,直接把事件传递给 InputDispatcher。

现在只要知道如何把一个按键的扫描码映射为按键码,InputReader 处理按键事件的整个流程都一清二楚了。

扫描码映射按键码

status_t EventHub::mapKey(int32_t deviceId, int32_t scanCode, int32_t usageCode, int32_t metaState,
                          int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags) const {
    std::scoped_lock _l(mLock);
    Device* device = getDeviceLocked(deviceId);
    status_t status = NAME_NOT_FOUND;
    if (device != nullptr) {
        // Check the key character map first.
        const std::shared_ptr<KeyCharacterMap> kcm = device->getKeyCharacterMap();
        if (kcm) {
            // 1. KeyCharacterMapFile :转换 scanCode 为 keyCode
            if (!kcm->mapKey(scanCode, usageCode, outKeycode)) {
                *outFlags = 0;
                status = NO_ERROR;
            }
        }
        // Check the key layout next.
        if (status != NO_ERROR && device->keyMap.haveKeyLayout()) {
            // 2. KeyLayoutFile: 把 scanCode 转换为 keycode
            if (!device->keyMap.keyLayoutMap->mapKey(scanCode, usageCode, outKeycode, outFlags)) {
                status = NO_ERROR;
            }
        }
        if (status == NO_ERROR) {
            if (kcm) {
                // 3. KeyCharacterMapFile: 根据meta按键状态,重新映射按键字符
                kcm->tryRemapKey(*outKeycode, metaState, outKeycode, outMetaState);
            } else {
                *outMetaState = metaState;
            }
        }
    }
    if (status != NO_ERROR) {
        *outKeycode = 0;
        *outFlags = 0;
        *outMetaState = metaState;
    }
    return status;
}

扫描码转化为按键码的过程有点小复杂

  • 首先根据 kcm(key character map) 文件进行转换。
  • 如果第一步失败,那么根据 kl(key layout) 文件进行转换。
  • 如果前两步,有一个成功,那么再根据meta按键状态,重新使用 kcm 文件对按键码再次进行转换。这个只对键盘起作用,例如按下 shift ,再按字母键,那么会产生大写的字母的按键码。而对于电源键和音量键,此步骤可以忽略。

可以发现,kcm 和 kl 文件都可以把按键的扫描码进行转换为按键码,然而 kcm 文件一般都只是针对键盘按键,而对于电源键和音量键,一般都是通过 kl 文件进行转换的。

那么如何找到输入设备的 kl 文件呢?前面通过 adb shell getevent 可以发现输入设备的文件节点为 /dev/input/event0,再通过 adb shell dumpsys input 导出所有设备的信息,就可以找到电源键属于哪个设备,以及设备的的按键配置文件

    6: qpnp_pon
      Classes: KEYBOARD
      Path: /dev/input/event0
      Enabled: true
      Descriptor: fb60d4f4370f5dbe8267b63d38dea852987571ab
      Location: qpnp_pon/input0
      ControllerNumber: 0
      UniqueId:
      Identifier: bus=0x0000, vendor=0x0000, product=0x0000, version=0x0000
      KeyLayoutFile: /system/usr/keylayout/Generic.kl
      KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
      ConfigurationFile:
      VideoDevice: <none>

从这个信息就可以看出,输入设备的 kl 文件为 /system/usr/keylayout/Generic.kl,它的电源键映射如下

key 116   POWER

其中,116是十进制,它的十六进制为 0x74,正好就是 adb shell getevent 显示的电源按键的扫描码。

POWER 就是被映射成的按键码,但是它是一个字符,而实际使用的是 int 类型,这个关系的映射是在下面定义的

// frameworks/native/include/android/keycodes.h
/**
 * Key codes.
 */
enum {
AKEYCODE_POWER = 26,
}

因此,电源按键的扫描码 0x74,被映射为按键码 26,正好就是上层 KeyEvent.java 定义的电源按键值

// frameworks/base/core/java/android/view/KeyEvent.java
public static final int KEYCODE_POWER           = 26;

在很早的 Android 版本上,配置文件中,电源键还会定义一个策略标志位,如下

key 116   POWER WAKE

其中,WAKE 就是一个策略标志位,在把扫描转换为按键码时,这个策略标志位也会被解析,它表示需要唤醒设备,上层会根据这个标志位,让设备唤醒。

结束

InputReader 处理按键事件的过程其实很简单,就是把按键事件交给 KeyboardInputMapper 处理,KeyboardInputMapper 根据配置文件,把按键的扫描码转换为按键码,并同时从配置文件中获取策略标志位,然后把这些信息包装成一个事件,发送到下一环。

现在,如果项目上让你完成功能按键的映射,或者解除某个按键的电源功能,你会了吗?

以上就是Input系统之InputReader处理按键事件详解的详细内容,更多关于InputReader处理按键事件的资料请关注我们其它相关文章!

(0)

相关推荐

  • Input系统之InputReader概要性实例分析

    目录 InputReader 的创建 EventHub 创建过程如下 InputReader 的运行 EventHub 提供事件 InputReader 的创建 从 InputManagerService: 创建与启动 可知,Input 系统的主要功能,主要集中在 native 层,并且Input 系统的 native 层又包含 InputReader, InputClassifer, InputDispatcher 三个子模块.本文来分析 InputReader 从创建到启动的基本流程,为后续

  • Input系统之InputReader处理触摸事件案例

    目录 正文 1. InputMapper 处理触摸事件 2. 收集触摸事件信息 3. 处理同步事件 3.1 同步数据 3.2 处理同步后的数据 3.2.1 加工数据 3.2.2 分发事件 结束 正文 手机一般有两种类型的输入设备.一种是键盘类型的输入设备,通常它包含电源键和音量下键.另一种是触摸类型的输入设备,触摸屏就属于这种类型. 键盘类型的输入设备一般都是产生按键事件,前面已经用几篇文章,分析了按键事件的分发流程. 触摸类型的输入设备一般都是产生触摸事件,本文就开始分析触摸事件的分发流程.

  • Input系统之InputReader处理合成事件详解

    目录 正文 生成合成事件 加载并解析输入设备的配置 InputReader 处理合成事件 创建与配置 InputDevice 配置基本参数 配置坐标系 配置 Surface 小结 正文 Input系统: InputReader 概要性分析 把 InputReader 的事件分为了两类,一类是合成事件,例如设备的增.删事件,另一类是元输入事件,也就是操作设备产生的事件,例如手指在触摸屏上滑动. 本文承接前文,以设备的扫描过程为例,分析合成事件的产生与处理过程.虽然设备的扫描过程只会生成部分合成事件

  • Input系统之InputReader处理按键事件详解

    目录 前言 认识按键事件 处理按键事件 扫描码映射按键码 结束 前言 前面几篇文章已经为 Input 系统的分析打好了基础,现在是时候进行更深入的分析了. 通常,手机是不带键盘的,但是手机上仍然有按键,就是我们经常使用的电源键以及音量键.因此还是有必要分析按键事件的处理流程. 那么,掌握按键事件的处理流程,对我们有什么用处呢?例如,手机上添加了一个功能按键,你知道如何把这个物理按键映射到上层,然后处理这个按键吗?又例如,如果设备是不需要电源键,但是系统默认把某一个按键映射为电源键,那么我们如何使

  • Android APP检测实体按键事件详解

    本文实例为大家分享了Android APP检测实体按键事件的具体代码,供大家参考,具体内容如下 一.检测点击按键事件一般不对手机上的输入按键进行处理,直接由系统按照默认情况操作.当然有时为了改善用户体验,需要让应用拦截按键事件,并进行额外处理. 要想监控按键事件,首先得知道每个按键的编码,这样才能根据不同的编码值进行相应的处理.监听器OnKeyListener只会检测控制键,不会检测文本键.实际测试发现HOME.SWICH.POWER普通的app是接收不到事件的,这几个事件在framew里面处理

  • vue 监听键盘回车事件详解 @keyup.enter || @keyup.enter.native

    vue运行为v-on在监听键盘事件时,添加了特殊的键盘修饰符: <input v-on:keyup.13="submit"> vue还非常贴心地给出了常用按键的别名,这样就不必去记keyCode ~ ~ 上面代码,还可以在这样写: <input v-on:keyup.enter="submit"> <input @keyup.enter="submit"> 全部的键盘别名: .enter .tab .delet

  • javascript数据代理与事件详解分析

    目录 数据代理与事件 回顾Object.defineProperty方法 何为数据代理 Vue中的数据代理 事件的基本使用 事件的修饰符 键盘事件 数据代理与事件 星光不负赶路人,满身花香蝶自来 回顾Object.defineProperty方法 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>回顾Object.defineproperty方法<

  • Android 广播大全 Intent Action 事件详解

    具体内容如下所示: Intent.ACTION_AIRPLANE_MODE_CHANGED; //关闭或打开飞行模式时的广播 Intent.ACTION_BATTERY_CHANGED; //充电状态,或者电池的电量发生变化 //电池的充电状态.电荷级别改变,不能通过组建声明接收这个广播,只有通过Context.registerReceiver()注册 Intent.ACTION_BATTERY_LOW; //表示电池电量低 Intent.ACTION_BATTERY_OKAY; //表示电池电

  • WPF中鼠标/键盘/拖拽事件以及用行为封装事件详解

    目录 鼠标事件 键盘输入事件 拖拽事件 用行为封装事件 用事件来实现 用行为来封装 本文主要介绍了WPF中常用的鼠标事件.键盘事件以及注意事项,同时使用一个案例讲解了拓展事件.除此之外,本文还讲述如何用行为(Behavior)来封装事件. Windows中的事件通过消息机制来完成,也就是Windows系统来捕获用户输入(如鼠标点击.键盘输入),然后Windows发送一个消息给应用程序,应用程序进行具体的处理.在Winform中,窗体中每个控件都是有独立的句柄,也就是每个控件都可以收到Window

  • Vue自定义事件(详解)

    前面的话 父组件使用props传递数据给子组件,子组件怎么跟父组件通信呢?这时,Vue的自定义事件就派上用场了.本文将详细介绍Vue自定义事件 事件绑定 每个 Vue 实例都实现了事件接口 (Events interface),即 使用 $on(eventName) 监听事件 使用 $emit(eventName) 触发事件 [注意]Vue 的事件系统分离自浏览器的EventTarget API.尽管它们的运行类似,但是 $on 和 $emit 不是addEventListener 和 disp

  • 基于BootStrap Metronic开发框架经验小结【五】Bootstrap File Input文件上传插件的用法详解

    Bootstrap文件上传插件File Input是一个不错的文件上传控件,但是搜索使用到的案例不多,使用的时候,也是一步一个脚印一样摸着石头过河,这个控件在界面呈现上,叫我之前使用过的Uploadify 好看一些,功能也强大些,本文主要基于我自己的框架代码案例,介绍其中文件上传插件File Input的使用. 1.文件上传插件File Input介绍 这个插件主页地址是:http://plugins.krajee.com/file-input,可以从这里看到很多Demo的代码展示:http:/

随机推荐