在测试 Android 设备时,经常会遇到输入中文的场景,切换键盘等操作繁琐易出问题 issue。
说到中文,最常见的字符集就是 GB2312,为了兼容一些繁体字就需要扩展版的 GBK,Big5(大五)。为了容纳全世界所有语言文字的编码,Unicode 协会开发了 Unicode 项目,总所周知 Unicode 是兼容 ASCII 的,而且市面上的操作系统 (桌面设备、手持便携设备) 都是支持 Unicode 字符集的,这里当然包括 Android。
Android 键盘允许输入的字符在 ASCII 集范围内,所以只要找到可以转换的字符和转换方式就能够,将字符最终表达为 ASCII 传给操作系统就可以实现中文输入,前提是不要产生乱码。
public static void main(String[] agrs) {
System.out.println("-------- result --------");
SortedMap availableCharsets = Charset.availableCharsets();
System.out.println(availableCharsets);
Charset defaultCharset = Charset.defaultCharset();
System.out.println(defaultCharset);
Charset UTF8 = Charset.forName("UTF-8");
Charset ASCII = Charset.forName("US-ASCII");
if (Charset.isSupported("UTF-7")) {
System.out.println("UTF-7 is supported.");
Charset UTF7 = Charset.forName("UTF-7");
String text = "中";
byte[] encoded = text.getBytes(UTF8);
System.out.println(encoded.length);
System.out.println(encoded[0]);
System.out.println(encoded[1]);
System.out.println(encoded[2]);
String str1 = new String(encoded, ASCII);
System.out.println(str1);
} else {
System.out.println("UTF-7 is not supported.");
}
System.out.println("\n------------------------");
}
有兴趣可以自己试一下,查看下当前环境支持的编码,默认编码等,下载此仓库 java-charset-sample。
UTF-8 和 UTF-16 等 UTF 标准定义了 Unicode 数值和字符的映射关系
ASCII 码一共规定了 128 个字符的编码,如大写的字母 A 是 65(二进制 01000001),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0。所以我们寻找到 7 位的字符表达就可以保证无乱码的方式与 ASCII 进行转换。
与其他 UTF 系列编码不同,满足我们的需要,UTF-7 转成 ASCII 刚好不会产生乱码,虽然 UTF-7 并不被 Unicode 标准正式认可。
UIAutomator 输入端:
Charset UTF7 = Charset.forName("UTF-7");
Charset ASCII = Charset.forName("US-ASCII");
byte[] encoded = text.getBytes(UTF7);
String str = new String(encoded, ASCII);
boolean result = element.setText(str);
此时我们的字符中文
已经被转成 &Ti1lhw-
这样的形式,通过实验,UTF-7 或 UTF-7 变种都以 &
或 +
作为开始,-
作为结束,天然的隔断,使得我们可以轻松的断字,处理起来方便很多,当一个字符完整技术后,触发字符提交即实现了中文输入。
UTF-8 没有字节序和 BOM 问题,而且 UTF-8 在互联网世界里使用也是最广泛的,但是 UTF-8 是一种变长的编码方式。它可以使用 1 ~ 4 个字节表示一个符号,根据不同的符号而变化字节长度。
Unicode 符号范围 | UTF-8 编码方式
(十六进制) |(二进制)
--------------------+------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
如我们测试 “中文” 的 “中” 字,Unicode 是 4E2D
,属于三个字节表示范围,即格式是 “1110xxxx 10xxxxxx 10xxxxxx”。然后,从 “中” 的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补 0。
当然,想利用 UTF 转换,也还是有办法的,比如我们可以利用 UTF-32 定长的特性,顺便说下,UTF-16 曾经是可以当定长编码使用,但是后期膨胀较快,Unicode 收录的字符很快就超过了 65536,所以如果还想用定长编码就只能采取 UTF-32,虽然浪费了空间和输入时间 (我们的场景里可以忽略这部分消耗),但是定长处理起来就没有了断字的困难。
我们可以将字逐一编码来实现,但是仍然会遇到上面出现的应该怎么隔断的问题,所以我们可以人工隔断。如下实现,将字符转成有 &
间隔的 16 进制 Unicode 字符串,避免了乱码。在 Android 输入端丢弃掉 &
结束前的按键响应,直到 &
将整个字符提交,实现中文的输入。
与 UTF-7 实现相比,键盘输入动作长度相等,耗时基本等价。
public static String string2Unicode(String string) {
StringBuffer unicode = new StringBuffer();
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
unicode.append(Integer.toHexString(c) + "&");
}
return unicode.toString();
}
boolean result = element.setText(string2Unicode(text));
StringBuffer string = new StringBuffer();
String str = "";
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
int c = getUnicodeChar(keyCode, event);
if (c == 0) {
return super.onKeyDown(keyCode, event);
}
if (c == '&') {
int data = Integer.parseInt(string.toString(), 16);
str = "" + (char) data;
InputConnection ic = getCurrentInputConnection();
ic.commitText(str, 1);
return true;
}
string.append((char) c);
return true;
}
Macaca 已经将如上的一种实现封装成了模块,集成到 Android 驱动中,我们直接启动输入法服务就可以了,用户无感知。
$ adb shell ime enable android.unicode.ime/.Utf7ImeService
$ adb shell ime set android.unicode.ime/.Utf7ImeService
欢迎讨论,互相学习。
微博: http://weibo.com/xudafeng
Github: https://github.com/xudafeng