上一篇文章分析了".kl"文件的解析,".kl"文件的作用是将linux scancode转换为Android keycode,相比之下".kcm"文件的解析要复杂一些。
“.kcm"文件意为按键字符映射文件,作用是将 Android按键代码与修饰符的组合映射到 Unicode字符,注意这里提到组合,意思是它可以提供组合按键功能,其实就目前的Android手机来说,基本都是全触摸屏,除了外接键盘,否则已经很少会用到”.kcm"文件了,但我们出于学习的目的还是来分析下其内部解析原理,和".kl"文件一样,系统也提供了一个名为 Generic.kcm的默认文件,它的语法是由键盘类型声明和一组按键声明组成的纯文本文件,例如:
type FULL
### Basic QWERTY keys ###
key A {
label: 'A'
base: 'a'
shift, capslock: 'A'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
}
FULL
指的是其键盘类型为全键盘,键盘类型声明通常会放在文件除注释外的顶部。
其他类型还有:
NUMERIC
:数字(12 键)键盘:
数字键盘支持使用多次击键方式输入文本,可能需要多次敲击键,才能生成所需的字母或符号。
这种类型的键盘通常设计为用拇指打字。
对应于 KeyCharacterMap.NUMERIC
。
-
PREDICTIVE
:一种具有所有字母的键盘,但每个键有多个字母:这种类型的键盘通常设计为用拇指打字。
对应于
KeyCharacterMap.PREDICTIVE
。 -
ALPHA
:一种具有所有字母的键盘,并且可能还带有一些数字。字母键盘支持文本直接输入,但由于尺寸小,因此布局可能会很紧凑。与
FULL
键盘相比,一些符号只能使用特殊的屏幕字符选择器才能输入。此外,为了提高打字速度和准确性,框架为字母键盘提供了特殊的功能,如自动首字母大写和切换/锁定 SHIFT 和 ALT 键。这种类型的键盘通常设计为用拇指打字。
-
FULL
:一种 PC 式全键盘:全键盘的用法类似于 PC 的键盘。通过按键盘上的键可以直接输入所有符号,无需屏幕支持或诸如自动首字母大写等直观功能。
这种类型的键盘通常设计为用双手打字。
-
SPECIAL_FUNCTION
:一种仅用于执行系统控制功能(而非打字)的键盘:特殊功能键盘仅由非实际用于打字的非打印键(如 HOME 和 POWER )组成。
-
接下来就来看看“.kcm“文件的具体解析规则:
status_t KeyMap::loadKeyCharacterMap(const InputDeviceIdentifier& deviceIdentifier, const std::string& name) { std::string path = getPath(deviceIdentifier, name, INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_CHARACTER_MAP); if (path.empty()) { return NAME_NOT_FOUND; } status_t status = KeyCharacterMap::load(path, KeyCharacterMap::FORMAT_BASE, &keyCharacterMap); if (status) { return status; } keyCharacterMapFile = path; return OK; }
其文件路径和".kl"文件一样,系统会搜索如下路径:"/odm/usr/*/", “/vendor/usr/*/”,"/system/usr/*/“,寻找后缀为".kcm"的文件,如果没找到则去"/data/system/devices/*/“目录下找,系统提供了默认的通用表"Generic.kcm"。
解析的入口函数为
KeyCharacterMap::load
,相比".kl"文件解析,这里多了一个参数KeyCharacterMap::FORMAT_BASE
,这是定义在KeyCharacterMap.h枚举值:enum Format { // Base keyboard layout, may contain device-specific options, such as "type" declaration. FORMAT_BASE = 0, // Overlay keyboard layout, more restrictive, may be published by applications, // cannot override device-specific options. FORMAT_OVERLAY = 1, // Either base or overlay layout ok. FORMAT_ANY = 2, };
KeyCharacterMap::load
status_t KeyCharacterMap::load(const std::string& filename, Format format, sp<KeyCharacterMap>* outMap) { outMap->clear(); Tokenizer* tokenizer; status_t status = Tokenizer::open(String8(filename.c_str()), &tokenizer); if (status) { ALOGE("Error %d opening key character map file %s.", status, filename.c_str()); } else { status = load(tokenizer, format, outMap); delete tokenizer; } return status; }
可以看到".kcm"文件和".kl"文件的解析基本是同样的套路,都是依靠
Tokenizer
这个工具类来处理文本文件,我们先把等下要用到的Tokenizer
的几个函数贴出来:Tokenizer的几个基本函数作用
isEof()函数:
/** * 返回当前读取到的字符是否是文本结尾 */ inline bool isEof() const { return mCurrent == getEnd(); }
skipDelimiters(const char* delimiters))函数:
/** * 跳过该行中指定的字符和空格. */ void skipDelimiters(const char* delimiters);
isEol()函数:
/** * 返回当前读取到的字符是否是文本结尾或者是否是行尾换行 */ inline bool isEol() const { return isEof() || *mCurrent == '\n'; }
peekChar()函数:
/** *获取当前位置的字符,文末返回空 */ inline char peekChar() const { return isEof() ? '\0' : *mCurrent; }
nextToken()函数:
/** * 在该行中查找指定字符的位置,并返回其位置之前的所有字符 */ String8 nextToken(const char* delimiters);
继续看KeyCharacterMap的重载函数load:
KeyCharacterMap::load
status_t KeyCharacterMap::load(Tokenizer* tokenizer, Format format, sp<KeyCharacterMap>* outMap) { status_t status = OK; //构造空的KeyCharacterMap sp<KeyCharacterMap> map = new KeyCharacterMap(); if (!map.get()) { ALOGE("Error allocating key character map."); status = NO_MEMORY; } else { ... Parser parser(map.get(), tokenizer, format); status = parser.parse(); ... if (!status) { *outMap = map; } } return status; }
解析核心类Parser
Parser::parse
status_t KeyCharacterMap::Parser::parse() { //只要没有到文件末尾,则一直循环 while (!mTokenizer->isEof()) { //跳过空格,tab键或者回车(非换行回车) mTokenizer->skipDelimiters(WHITESPACE); //判断当前字符是否到了文本结尾或者一行的结尾或者是否遇到了注释"#" if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { //mState默认值为STATE_TOP switch (mState) { case STATE_TOP: { String8 keywordToken = mTokenizer->nextToken(WHITESPACE); //keywordToken为每一行开头第一个字符串,分成了三个分支处理 //这里说明了".kcm"文件没行开头只能有三种合法字符串 if (keywordToken == "type") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseType(); if (status) return status; } else if (keywordToken == "map") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseMap(); if (status) return status; } else if (keywordToken == "key") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseKey(); if (status) return status; } else { ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(), keywordToken.string()); return BAD_VALUE; } break; } case STATE_KEY: { status_t status = parseKeyProperty(); if (status) return status; break; } } mTokenizer->skipDelimiters(WHITESPACE); if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { ALOGE("%s: Expected end of line or trailing comment, got '%s'.", mTokenizer->getLocation().string(), mTokenizer->peekRemainderOfLine().string()); return BAD_VALUE; } } //下一行 mTokenizer->nextLine(); } .... .... return NO_ERROR; }
这个函数大概有两部分,第一部分在while循环中一直解析".kcm"文件,第二部分为解析完成之后的一些状态判断,以此来判定此次解析是否没有异常,我们的重点是关注while循环中的代码。
我们以一个实际例子来看其解析规则,我们就当".kcm"文件只有这么一点,
type FULL ### Basic QWERTY keys ### key A { label: 'A' base: 'a' shift, capslock: 'A' }
这个函数代表跳过指定字符,这里即跳过空格,tab键或者回车(非换行回车)
mTokenizer->skipDelimiters(WHITESPACE)static const char* WHITESPACE = " \t\r";
跳过之后判断当前字符是否到了文本结尾或者一行的结尾或者是否遇到了注释"#",如果这几种情况都不是则继续该行解析,否则换到下一行。
接着来到一个swich…case,
mState
在构造Parser
时给了一个默认值STATE_TOP
,所以进去STATE_TOP
分支:.... .... case STATE_TOP: { String8 keywordToken = mTokenizer->nextToken(WHITESPACE); if (keywordToken == "type") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseType(); if (status) return status; } else if (keywordToken == "map") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseMap(); if (status) return status; } else if (keywordToken == "key") { mTokenizer->skipDelimiters(WHITESPACE); status_t status = parseKey(); if (status) return status; } else { ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(), keywordToken.string()); return BAD_VALUE; } break; } ... ...
这里调用的
mTokenizer->nextToken
会找到每一行的第一个不包含空格,tab键或者回车的字符串,我们例子中第一行的keywordToken
是"type",所以继续进入此分支:KeyCharacterMap::Parser::parseType
status_t KeyCharacterMap::Parser::parseType() { if (mMap->mType != KEYBOARD_TYPE_UNKNOWN) { //已经解析过"type"了,这里可以看出".kcm"文件的type关键字只能有一行 ALOGE("%s: Duplicate keyboard 'type' declaration.", mTokenizer->getLocation().string()); return BAD_VALUE; } KeyboardType type; //拿到"type"之后的字符串 String8 typeToken = mTokenizer->nextToken(WHITESPACE); if (typeToken == "NUMERIC") { type = KEYBOARD_TYPE_NUMERIC; } else if (typeToken == "PREDICTIVE") { type = KEYBOARD_TYPE_PREDICTIVE; } else if (typeToken == "ALPHA") { type = KEYBOARD_TYPE_ALPHA; } else if (typeToken == "FULL") { type = KEYBOARD_TYPE_FULL; } else if (typeToken == "SPECIAL_FUNCTION") { ALOGW("The SPECIAL_FUNCTION type is now declared in the device's IDC file, please set " "the property 'keyboard.specialFunction' to '1' there instead."); // TODO: return BAD_VALUE here in Q type = KEYBOARD_TYPE_SPECIAL_FUNCTION; } else if (typeToken == "OVERLAY") { type = KEYBOARD_TYPE_OVERLAY; } else { ALOGE("%s: Expected keyboard type label, got '%s'.", mTokenizer->getLocation().string(), typeToken.string()); return BAD_VALUE; } //将实际type保存到mType mMap->mType = type; return NO_ERROR; }
这个函数很简单,只是根据不同的键盘类型"type",将类型对应值保存到
mMap->mType
,我们例子中"type"为"FULL",即将KEYBOARD_TYPE_FULL
保存到mMap->mType
。
到此例子的第一行就解析完了,接着下一行,每一行的解析都是按照同样的规则,下一个有效行又同样走到swich…case中,mState
的值并没有变化,这时的keywordToken
就拿到为"key",会调用parseKey()
来处理:key A { label: 'A' base: 'a' shift, capslock: 'A' }
KeyCharacterMap::Parser::parseKey
status_t KeyCharacterMap::Parser::parseKey() { String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE); //这里拿到的keyCodeToken为字符"A" int32_t keyCode = getKeyCodeByLabel(keyCodeToken.string()); //得到Android键盘码,A对应29 if (!keyCode) { //如果Android键盘码为0,“AKEYCODE_UNKNOWN = 0” ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(), keyCodeToken.string()); return BAD_VALUE; } if (mMap->mKeys.indexOfKey(keyCode) >= 0) { //如果已经解析过字符"A" ALOGE("%s: Duplicate entry for key code '%s'.", mTokenizer->getLocation().string(), keyCodeToken.string()); return BAD_VALUE; } mTokenizer->skipDelimiters(WHITESPACE); String8 openBraceToken = mTokenizer->nextToken(WHITESPACE); //openBraceToken拿到为“A”之后的字符“{” if (openBraceToken != "{") { //如果不是"{" ALOGE("%s: Expected '{' after key code label, got '%s'.", mTokenizer->getLocation().string(), openBraceToken.string()); return BAD_VALUE; } ... mKeyCode = keyCode; mMap->mKeys.add(keyCode, new Key()); mState = STATE_KEY; return NO_ERROR; }
函数开头拿到了字符"A",然后调用
getKeyCodeByLabel
函数,这个函数定义在InputEventLabels.h中,我们在上一篇文章已经详细分析过,它的主要作用就是传过去的字符"A",返回通过宏DEFINE_KEYCODE
拼接而成的AKEYCODE_A
的值,AKEYCODE_A
定义在keycodes.h中,为29:/** 'A' key. */ AKEYCODE_A = 29,
接着继续通过
mTokenizer->nextToken
拿"A"之后的字符,openBraceToken
的值就为" { ",如果不是这个字符即为非法情况,最后将"A"的Android键盘码29保存到mKeyCode
,并以此为键,构造一个对象Key(这是一个空对象,其成员变量都是默认值),放入集合mKeys
中,这里我们就知道每一个按键都会对应一个对象Key。这一行的解析就结束了,Key是定义在KeyCharacterMap.h中的结构体:
struct Key { Key(); Key(const Key& other); ~Key(); /* The single character label printed on the key, or 0 if none. */ char16_t label; /* The number or symbol character generated by the key, or 0 if none. */ char16_t number; /* The list of key behaviors sorted from most specific to least specific * meta key binding. */ Behavior* firstBehavior; };
并且注意这里会将
mState
修改为STATE_KEY
,继续下一行,接着又会回到swich…case中,但此时的mState
已经变成了STATE_KEY
,所以会走STATE_KEY
的分支:case STATE_KEY: { status_t status = parseKeyProperty(); if (status) return status; break; }
这里直接调用函数
parseKeyProperty
,这个函数代码比较多,它的主要作用是解析按键"A"大括号里面的内容:key A { label: 'A' base: 'a' shift, capslock: 'A' }
KeyCharacterMap::Parser::parseKeyProperty
status_t KeyCharacterMap::Parser::parseKeyProperty() { //通过"A"的Android键盘码29获取对应的对象Key Key* key = mMap->mKeys.valueFor(mKeyCode); //这里nextToken函数传入了一个不同的参数WHITESPACE_OR_PROPERTY_DELIMITER //WHITESPACE_OR_PROPERTY_DELIMITER = " \t\r,:"; //作用是拿到下一个空格或者tab或者回车或者','或者':'之前的的字符串 String8 token = mTokenizer->nextToken(WHITESPACE_OR_PROPERTY_DELIMITER); //这里的token拿到的即是例子中的"label" if (token == "}") { //如果是"}",代表按键"A"的大括号中已经全部解析完成 mState = STATE_TOP; return finishKey(key); } //这个集合用来存储大括号中冒号":"之前的字符串所构造的Property结构体集合 //可能有一个,也可能有多个 Vector<Property> properties; // 解析所有以逗号分隔的属性名称,直到第一个冒号为止。 for (;;) { if (token == "label") { properties.add(Property(PROPERTY_LABEL)); } else if (token == "number") { properties.add(Property(PROPERTY_NUMBER)); } else { //除"label"和"number"的行走这个分支 int32_t metaState; status_t status = parseModifier(token.string(), &metaState); if (status) { ALOGE("%s: Expected a property name or modifier, got '%s'.", mTokenizer->getLocation().string(), token.string()); return status; } properties.add(Property(PROPERTY_META, metaState)); } mTokenizer->skipDelimiters(WHITESPACE); //不是一行的结尾 if (!mTokenizer->isEol()) { char ch = mTokenizer->nextChar(); //遇到冒号":" if (ch == ':') { break; //遇到逗号"," } else if (ch == ',') { mTokenizer->skipDelimiters(WHITESPACE); token = mTokenizer->nextToken(WHITESPACE_OR_PROPERTY_DELIMITER); continue; } } ALOGE("%s: Expected ',' or ':' after property name.", mTokenizer->getLocation().string()); return BAD_VALUE; } //到这里这个for循环结束代表已经读到冒号":"之后了 //跳过冒号之后的一大段空格 mTokenizer->skipDelimiters(WHITESPACE); //解析的每一行对应一个Behavior对象 Behavior behavior; bool haveCharacter = false; bool haveFallback = false; bool haveReplacement = false; //来到一个do...while循环 do { //得到当前字符 char ch = mTokenizer->peekChar(); //如果字符为" ' ",这是单引号的左半部分 if (ch == '\'') { char16_t character; //处理行末字符,对于包含反斜杠'\'的情况特殊处理 status_t status = parseCharacterLiteral(&character); if (status || !character) { ALOGE("%s: Invalid character literal for key.", mTokenizer->getLocation().string()); return BAD_VALUE; } if (haveCharacter) { ALOGE("%s: Cannot combine multiple character literals or 'none'.", mTokenizer->getLocation().string()); return BAD_VALUE; } if (haveReplacement) { ALOGE("%s: Cannot combine character literal with replace action.", mTokenizer->getLocation().string()); return BAD_VALUE; } //将行末字符保存到behavior.character behavior.character = character; haveCharacter = true; } else {//else代表大括号内某行的最后部分不是单引号引用的,例如以fallback,none结尾 //本篇文章将不会去分析这类按键的作用 token = mTokenizer->nextToken(WHITESPACE); if (token == "none") { ... } else if (token == "fallback") { ... } else if (token == "replace") { ... } else { ... return BAD_VALUE; } } mTokenizer->skipDelimiters(WHITESPACE); //退出条件是解析到该行或者整个文本的末尾,或者遇到注释"#" } while (!mTokenizer->isEol() && mTokenizer->peekChar() != '#'); // 遍历properties for (size_t i = 0; i < properties.size(); i++) { const Property& property = properties.itemAt(i); switch (property.property) { case PROPERTY_LABEL: if (key->label) { ALOGE("%s: Duplicate label for key.", mTokenizer->getLocation().string()); return BAD_VALUE; } //属性为"label" key->label = behavior.character; break; case PROPERTY_NUMBER: if (key->number) { ALOGE("%s: Duplicate number for key.", mTokenizer->getLocation().string()); return BAD_VALUE; } //属性为"number" key->number = behavior.character; break; case PROPERTY_META: { for (Behavior* b = key->firstBehavior; b; b = b->next) { if (b->metaState == property.metaState) { ALOGE("%s: Duplicate key behavior for modifier.", mTokenizer->getLocation().string()); return BAD_VALUE; } } //非"label"和"number"属性,构造Behavior对象,组成链表 Behavior* newBehavior = new Behavior(behavior); newBehavior->metaState = property.metaState; newBehavior->next = key->firstBehavior; //key->firstBehavior为链表头部,指向最后添加的Behavior key->firstBehavior = newBehavior; ... break; } } } return NO_ERROR; }
这个函数的主要目的是解析按键的大括号内的字符串。
它首先会通过"A"的键盘码29,获取其对应的结构体
Key
,这时Key
只是一个空对象,其成员变量都还没有赋值,它将会根据接下来解析到的内容来赋值。接着声明了一个存储类型为
Property
集合properties
,这个集合的作用是用来存储例子中例如属性"label",“base”,“shift, capslock”。key A { label: 'A' base: 'a' shift, capslock: 'A' }
每一行都会对应一个
properties
,像"label",“base"的行properties
的size为1,像"shift, capslock"的行properties
的size就为2,这部分的实现是依靠for ( ;; )这个死循环来进行的,这个循环中遇到冒号”:“就会退出,遇到逗号”,"就会继续循环。上述for ( ;; )循环中对于属性为"label"和"number"会直接构造一个
Property
对象,对于其他类型,则会调用parseModifier
函数进一步处理,这个函数主要作用是对于属性中包含"+"或者"0"的情况作一些处理,我们例子中没这种情况所以不细看了。for ( ;; )结束之后大括号中的属性值就解析完了,接着会构造
Behavior
结构体,同样是初始化状态,其变量会在后面解析赋值。接着又是一个do…while循环,这个循环的目的是解析冒号":“之后的内容,退出条件是解析到行的或者文本的末尾或者遇到注释”#",因为在循环之前已经调用过
mTokenizer->skipDelimiters(WHITESPACE);
跳过冒号之后的所有空格,所以循环中调用peekChar
就会直接得到例子中一行的最后的左边单引号 " ’ “,当前”.kcm"文件中除了行末尾是单引号的情况,还有一种没有单引号,例如下面这种:key ESCAPE { base: none alt, meta: fallback HOME ctrl: fallback MENU }
这种情况就会再细分处理,由于本篇文章的例子不是这种情况,就不去细看了,还是回到
peekChar
得到单引号的情况,会调用parseCharacterLiteral
函数:KeyCharacterMap::Parser::parseCharacterLiteral
status_t KeyCharacterMap::Parser::parseCharacterLiteral(char16_t* outCharacter) { //返回值和peekChar一样 char ch = mTokenizer->nextChar(); if (ch != '\'') { goto Error; } //这里再调用nextChar获得的就是左边单引号之后的字符 ch = mTokenizer->nextChar(); //左单引号之后的字符为"\" if (ch == '\\') { ...... //左单引号之后的字符为"a-z","A-Z","0-9","~,!,@,#,{,(....",并且不为右单引号 } else if (ch >= 32 && ch <= 126 && ch != '\'') { // ASCII literal character. *outCharacter = ch; } else { goto Error; } ch = mTokenizer->nextChar(); if (ch != '\'') { //如果不是以右单引号结尾则返回异常 goto Error; } // Ensure that we consumed the entire token. if (mTokenizer->nextToken(WHITESPACE).isEmpty()) { return NO_ERROR; } Error: ALOGE("%s: Malformed character literal.", mTokenizer->getLocation().string()); return BAD_VALUE; }
这里先说说
peekChar
和nextChar
的区别,这两个函数都会返回当前读取到的同一个字符,只不过nextChar
在返回之后,当前字符会向后移一位。inline char peekChar() const { return isEof() ? '\0' : *mCurrent; } inline char nextChar() { return isEof() ? '\0' : *(mCurrent++); }
parseCharacterLiteral
函数的原理也很简单,就是调用多个nextChar
函数,使得当前读取的字符往后一位一位的移动,上述函数省略的大段代码是对如下这种情况的处理,左单引号之后跟反斜杠"\":key ENTER { label: '\n' base: '\n' }
我们例子的情况属于:
“左单引号之后的字符为"a-z”,“A-Z”,“0-9”,"~,!,@,#,{,(…",并且不为右单引号"key A { label: 'A' base: 'a' shift, capslock: 'A' }
所以直接将字符’A’赋值给传进来的参数
outCharacter
,最后行末一定是以右单引号结尾的,否则报错。好了do…while循环就结束了,就是将读取到的行末的字符保存到了
Behavior
结构体的变量character
中,其变量haveCharacter
置为true。接着
parseKeyProperty
函数的最后一段代码,又是一个for循环,遍历properties
集合,拿到每一个Property
对象之后通过其成员变量property.property
进行分类,三种类型:PROPERTY_LABEL,PROPERTY_NUMBER,PROPERTY_META分别对应按键的三种属性,“label”,“number"和其他,对于"label”,“number”,一个Property
对象就对应其所在的一行,而其他属性可能一行有多个Property
对象,例如例子中的shift, capslock
。最后的for循环中对于"label","number"属性仅仅是将其解析出来的值,存入按键对应的
Key
对象的成员变量label
和number
中,而对于非label
和number
属性则会为按键构造一个Behavior
链表,由按键对应的Key
对象的成员变量firstBehavior
作为链表头部链接着,firstBehavior
总是指向最后添加进来的Behavior
。当按键所有属性解析完成之后就该处理
parseKeyProperty
函数开头的遇到" } "的情况以结束当前解析,修改状态mState
为STATE_TOP
,并调用finishKey
函数:KeyCharacterMap::Parser::finishKey
status_t KeyCharacterMap::Parser::finishKey(Key* key) { // Fill in default number property. if (!key->number) { char16_t digit = 0; char16_t symbol = 0; for (Behavior* b = key->firstBehavior; b; b = b->next) { //依次拿到非"lable"和"number"的所有属性的值 char16_t ch = b->character; if (ch) { if (ch >= '0' && ch <= '9') { digit = ch; } else if (ch == '(' || ch == ')' || ch == '#' || ch == '*' || ch == '-' || ch == '+' || ch == ',' || ch == '.' || ch == '\'' || ch == ':' || ch == ';' || ch == '/') { symbol = ch; } } } key->number = digit ? digit : symbol; } return NO_ERROR; }
这个函数主要作用是对于没有"number"属性的按键赋以默认值,规则是遍历
Behavior
链表,寻找符合条件的赋值情况,每一行对应一个Behavior
对象,注意上面for循环中并没有break或者return,意味着需要依次拿到所有Behavior
的值character
,并找到最后一个满足条件的值才能决定按键属性"number"的默认值。
"number"默认值的条件就是:- 有非"lable"和"number"属性值大于0。
- 满足1的情况下非"lable"和"number"属性值有为"0-9"的,则"number"默认值赋为
character
。 - 满足1的情况下非"lable"和"number"属性值有为函数中列出的这些符号,则"number"默认值赋为
character
。 - 如果条件1都不满足,则"number"默认值赋为0,例如我们举的例子中的情况。
从上面"number"赋默认值的函数可以看出,"number"这个属性的值只能为数字或者特殊符号。
到此
parseKeyProperty
函数就已经全部分析完了,这个函数主要功能就是解析按键大括号中的部分,主要对按键属性,以及其值进行解析,解析完成之后存入对应结构体,我们主要看到了三个结构体,对应一个按键的结构体Key
,对应一个属性的结构体Property
,对应大括号中一行的结构体Behavior
。我们再来说说
Behavior
,它代表着属性的行为,每个属性都会映射到一个行为。最常见的行为是输入字符,但还有其他行为。例如我们举的例子中:
key A { label: 'A' base: 'a' shift, capslock: 'A' }
base
属性代表着这个按键最基本的行为,其属性值对应’a’,也就是说点击按键A最基本的行为就是输入字符’a’,base
还有一些属性值,如none
,即点击之后不会输入任何字符。另外还有一些组合行为,组合意思是多个按键一起点击,我们看到例子中最后一行有shift,capslock属性,当我们同时点击shift + A或者capslock + A时即会输出’A’。
".kcm"文件更多的应用在物理键盘上,目前Android智能设备已经很少用了。