Android触摸屏虚拟按键实现方法

    在前几年流行的手机上,一般都会在底部有触摸按键功能(HOME、BACK等键),而这些触摸按键有些是直接用触摸屏的指定区域来模拟,有些是使用屏下的传感器来检测,而我们本次主要是讨论触摸屏的方式,一般情况下,我们直接在驱动里获取我们要模拟的多个指定区域,判断当前触摸的范围是否在指定区域,是则报以相应的KEYCODE,而最近在阅读https://source.android.com/devices/input/touch-devices这篇文档时, 知道在Android系统里也可能在驱动里面将预定好的区域坐标及键码配置好,由Android Frameworks来获取并进行处理,而我们只需要按要求设计好相应的区域和配置好文件即可,根据该文档,我们需要做如下一些配置步骤(下面以Android7.1源码来说明):
    1.在实现虚拟按键时,内核必须有映射名为virtualkeys.<devicename>的虚拟按键映射文件,如触摸屏设备驱动程序里的设备名为touchyfeely,则虚拟按键映文件的路径必须为:
      /sys/board_properties/virtualkeys.touchyfeely
      虚拟按键映射文件描述了触摸屏上虚拟按键的坐标和Linux按键代码,该文件是一个纯文本文件,由一系列换行符或冒号分隔的虚拟按键布局描述组成。相应的语法要求如下:
      注释行以“#”开头,只对本行有效。
      每个虚拟按键由6个冒号分隔的数据进行描述:
  • 0x01:版本代码。必须始终为 0x01。
  • <Linux key code>:虚拟按键的 Linux 按键代码。
  • <centerX>:虚拟按键中心的 X 轴坐标(以像素为单位)。
  • <centerY>:虚拟按键中心的 Y 轴坐标(以像素为单位)。
  • <width>:虚拟按键的宽度(以像素为单位)。
  • <height>:虚拟按键的高度(以像素为单位)。
      所有的坐标和尺寸都是根据显示坐标系指定的。
      下面是虚拟按键映射文件的两种格式:
      a.一行
        # All on one line
        0x01:158:55:835:90:55:0x01:139:172:835:125:55:0x01:102:298:835:115:55:0x01:217:412:835:95:55
      b.多行
          # One key per line
          0x01:158:55:835:90:55
          0x01:139:172:835:125:55
          0x01:102:298:835:115:55
          0x01:217:412:835:95:55
      这两种格式的示例数据是在一款480*800分辨率的触摸屏上,虚拟按键的centerY坐标为835,比触摸屏的可见区域略低的位置。
    2.按键布局及字符映射
       有了上面的虚拟按键映射文件在内核的实现后,还需要对应的kl和kcm文件,下面是针对上面例子对应的配置文件:
       a.按键布局文件(/system/usr/keylayout/touchyfeely.kl)
            key 158 BACK
            key 139 MENU
            key 102 HOME
            key 217 SEARCH
       b.按键字符映射文件(/system/usr/keychars/touchyfeely.kcm)
          type SPECIAL_FUNCTION
    3.为什么是virtualkeys.<devicename>
       在frameworks/native/services/inputflinger/EventHub.cpp文件中,我们可以看到如下内容:
       status_t EventHub::loadVirtualKeyMapLocked(Device* device) {
           // The virtual key map is supplied by the kernel as a system board property file.
           String8 path;
           path.append("/sys/board_properties/virtualkeys.");
           path.append(device->identifier.name);
           if (access(path.string(), R_OK)) {
              return NAME_NOT_FOUND;
           }
           return VirtualKeyMap::load(path, &device->virtualKeyMap);
       }
       从上面两句path.append可以看出最终的path值就是/sys/board_properties/virtualkeys.<devicename>了。
    4.文件内容为何是一行或多行以“:”分隔的文本内容?
       上面代码继续往下跟,可以追溯到frameworks/native/libs/input/VirtualKeyMap.cpp文件,代码如下:
        status_t VirtualKeyMap::load(const String8& filename, VirtualKeyMap** outMap) {
            *outMap = NULL;

            Tokenizer* tokenizer;
            status_t status = Tokenizer::open(filename, &tokenizer);
            if (status) {
                ALOGE("Error %d opening virtual key map file %s.", status, filename.string());
            } else {
                VirtualKeyMap* map = new VirtualKeyMap();
                if (!map) {
                    ALOGE("Error allocating virtual key map.");
                    status = NO_MEMORY;
                } else {
        #if DEBUG_PARSER_PERFORMANCE
                    nsecs_t startTime = systemTime(SYSTEM_TIME_MONOTONIC);
        #endif
                    Parser parser(map, tokenizer);
                    status = parser.parse();
        #if DEBUG_PARSER_PERFORMANCE
                    nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime;
                    ALOGD("Parsed key character map file '%s' %d lines in %0.3fms.",
                            tokenizer->getFilename().string(), tokenizer->getLineNumber(),
                            elapsedTime / 1000000.0);
        #endif
                    if (status) {
                        delete map;
                    } else {
                        *outMap = map;
                    }
                }
                delete tokenizer;
            }
            return status;
        }
       从上面代码可以看到重点语句status = parser.parse();,即会调用到同一文件的parse函数,其代码如下:
        status_t VirtualKeyMap::Parser::parse() {
            while (!mTokenizer->isEof()) {
        #if DEBUG_PARSER
                ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().string(),
                        mTokenizer->peekRemainderOfLine().string());
        #endif

                mTokenizer->skipDelimiters(WHITESPACE);

                if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
                    // Multiple keys can appear on one line or they can be broken up across multiple lines.
                    do {
                        String8 token = mTokenizer->nextToken(WHITESPACE_OR_FIELD_DELIMITER);
                        if (token != "0x01") {
                            ALOGE("%s: Unknown virtual key type, expected 0x01.",
                                  mTokenizer->getLocation().string());
                            return BAD_VALUE;
                        }

                        VirtualKeyDefinition defn;
                        bool success = parseNextIntField(&defn.scanCode)
                                && parseNextIntField(&defn.centerX)
                                && parseNextIntField(&defn.centerY)
                                && parseNextIntField(&defn.width)
                                && parseNextIntField(&defn.height);
                        if (!success) {
                            ALOGE("%s: Expected 5 colon-delimited integers in virtual key definition.",
                                  mTokenizer->getLocation().string());
                            return BAD_VALUE;
                        }

        #if DEBUG_PARSER
                        ALOGD("Parsed virtual key: scanCode=%d, centerX=%d, centerY=%d, "
                                "width=%d, height=%d",
                                defn.scanCode, defn.centerX, defn.centerY, defn.width, defn.height);
        #endif
                        mMap->mVirtualKeys.push(defn);
                    } while (consumeFieldDelimiterAndSkipWhitespace());

                    if (!mTokenizer->isEol()) {
                        ALOGE("%s: Expected end of line, got '%s'.",
                                mTokenizer->getLocation().string(),
                                mTokenizer->peekRemainderOfLine().string());
                        return BAD_VALUE;
                    }
                }

                mTokenizer->nextLine();
            }

            return NO_ERROR;

        }
        从上面可以看到为何以"0x01"开头了吧,接下来再调用parseNextIntField函数来解析剩下的5个数据,该函数代码如下:
        bool VirtualKeyMap::Parser::parseNextIntField(int32_t* outValue) {
            if (!consumeFieldDelimiterAndSkipWhitespace()) {
                return false;
            }

            String8 token = mTokenizer->nextToken(WHITESPACE_OR_FIELD_DELIMITER);
            char* end;
            *outValue = strtol(token.string(), &end, 0);
            if (token.isEmpty() || *end != '\0') {
                ALOGE("Expected an integer, got '%s'.", token.string());
                return false;
            }
            return true;
        }
        从上面可以看出这些数据都会转为整型,其中分隔符WHITESPACE_OR_FIELD_DELIMITER定义如下:
        static const char* WHITESPACE_OR_FIELD_DELIMITER = " \t\r:";
    5.上面分析了驱动的虚拟按键映射文件对应的解析需求,那么我们实际的驱动文件要如何在原有的触摸屏驱动基础上添加相应的文件结点生成处理呢?(当然也可以独立为一个驱动来实现)
        下面是抽象出来的映射文件生成及数据处理需要的代码:
        #define TOUCHYFEELY_KEY_HOME    102
        #define TOUCHYFEELY_KEY_MENU    139
        #define TOUCHYFEELY_KEY_BACK      158
        #define TOUCHYFEELY_KEY_SEARCH  217

        static ssize_t virtual_keys_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
        {
            return sprintf(buf,
            __stringify(EV_KEY) ":" __stringify(TOUCHYFEELY_KEY_HOME) ":298:835:115:55"
             ":" __stringify(EV_KEY) ":" __stringify(TOUCHYFEELY_KEY_MENU) ":172:835:125:55"
             ":" __stringify(EV_KEY) ":" __stringify(TOUCHYFEELY_KEY_BACK) ":55:835:90:55"
             ":" __stringify(EV_KEY) ":" __stringify(TOUCHYFEELY_KEY_SEARCH) ":412:835:95:55"
             "\n");
        }

        static struct kobj_attribute virtual_keys_attr = {
            .attr = {
                .name = "virtualkeys.touchyfeely", 
                .mode = S_IRUGO,
            },
            .show = &virtual_keys_show,
        };

        static struct attribute *properties_attrs[] = {
                &virtual_keys_attr.attr,
                NULL
        };

        static struct attribute_group properties_attr_group = {
            .attrs = properties_attrs,
        };

        static void touchyfeely_virtual_keys_init(void)
        {
            int ret;
            struct kobject *properties_kobj;
    
            properties_kobj = kobject_create_and_add("board_properties", NULL);
            if (properties_kobj)
                ret = sysfs_create_group(properties_kobj,  &properties_attr_group);
            if (!properties_kobj || ret)
                pr_err("failed to create board_properties\n");    
        }
        接下来,我们需要在probe函数调用上面的init函数,代码如下:
        static int touchyfeely_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
        {
            /*...*/

            touchyfeely_virtual_keys_init();
 
       /*...*/

            set_bit(KEY_HOME,  input_dev->keybit);
            set_bit(KEY_MENU,  input_dev->keybit);
            set_bit(KEY_BACK,  input_dev->keybit);
            set_bit(KEY_SEARCH,  input_dev->keybit);

            /*...*/
        }
        至此,我们对虚拟按键需要实现的内容和配置文件有了清晰的了解。
    6.参考网址
        http://www.phonesdevelopers.info/1690353/
        http://www.cnblogs.com/aceheart/archive/2012/10/27/2742309.html

评论

此博客中的热门博文

I/O映射之I/O端口

通过Netlink检测网线插拔

使用seq_file