模拟键盘输入优化
最近又看到用户反馈里有人喷咱这个模拟键盘输入体验太差,这正好有空就给他优化了。
当前实现
伪代码警告
有个二维数组描述按键位置:
1 | const POS = [ |
一个 map 把按钮都放进去:
1 | {POS.map((linePos, lineIndex) => |
Key 组件用于聚合操作:
1 | const Key = () => |
现有问题
虽然用户也没办法准确描述自己遭遇的「不好用」具体指什么,但大概应该可以总结为「想输入某字符的时候点击了按钮,但上屏的是另一个字符」。冲突点在于「我以为我点了A」和「实际上屏了B」。
相对应的解决方案也显而易见:
一,在上屏 B 的时候让用户知道自己点了 B;
二,准确的上屏用户想点的字符 A;
优化方向
观察原生键盘,其实这两个优化方向早就已经实装运用了:
一,iOS 键盘在触摸的时候会有更大的字符展示在已点击字母的上方;
二,根据用户输入习惯、语法进行预测;
方案一其实稍微改改 Key 组件,在用户按下的时候展示新样式就行。
方案二呢?
输入预测
鉴于目前的输入场景是单词拼写,输入内容和上下文基本无关,反倒是和用户已掌握词汇量强相关–毕竟人只会输入掌握了的词–而不是语法语态、或是用户的常用表达习惯。
以字母为单位,N-gram 处理这个场景就很合适:
对用户掌握的词表进行统计,获得不同字母组合下新字母出现的概率。再根据输入匹配,附以不同权重汇总最终概率,即预测输入。
所以准备了一份四六级单词表去重后统计 0-1-2 字符 N-gram:
用以匹配用户输入(的末尾),计算出下一个字符出现的概率:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36function interpolateFromSortedNgram({
context,
lambdas = [0.6, 0.3, 0.1],
}: {
context: string;
lambdas?: number[];
}) {
const result: Record<string, number> = {};
const seen = new Set();
// 辅助函数:汇总到 result
const decode = (record: Record<string, number>, lambda: number) => {
if (!record) return;
for (char of record) {
const weight = record[char]
result[char] = (result[char] || 0) + weight;
seen.add(char);
}
};
// 提取上下文
const triKey = context.slice(-2);
const biKey = context.slice(-1);
decode(NGRAM.trigram[triKey], lambdas[0]);
decode(NGRAM.bigram[biKey], lambdas[1]);
decode(NGRAM.unigram, lambdas[2]);
// 归一化权重(确保总和为 1)
const total = Object.values(result).reduce((a, b) => a + b, 0);
for (const char of seen) {
result[char] = +(result[char] / total).toFixed(4);
}
return result;
}
输入 ‘RE’ 之后的结果:
最后适当的修改这些字符触发面积即可:
收尾
这份 ngram 统计结果跑出来有 76KB,当然我不想为每个用户都增加这么大的文件。所以考虑另一种方案:具体字母概率不重要,在权重的影响下只需考虑可能出现的字母概率顺序。
讲人话:当前场景并不需要对预测进行排序,而是需要尽可能预测出几个字母,进行触发区放大。
所以统计数值就变成了:
还可以加上:如果用户正在输入标准答案,则提高下一字母的概率。
所以稍微改一改代码:
1 | // 辅助函数:将排序字符串转成带权重对象(1/i 映射) |
而这份 ngram2.json 只有 8KB,好多了。
俺这儿只是对特定场景造轮子,对比有专业团队长时间维护的原生键盘,颇有种班门弄斧的感觉,专业的事情确实还得专业的人做。
倒是从统计数据里看到一些有意思的东西:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
"unigram": "eiatrnoslcupdmghybfvwkxqzj", // 字母开头概率顺序
"bigram": {
...
"q": "u", // q 后面只能跟 u
...
},
"trigram": {
...
"bj": "e", // 类似的,这些组合后面只会跟某一个字母
"hd": "r",
"hs": "t",
"gd": "o",
"wt": "h",
...
}
}