安卓中的虚拟键盘实现,KeyEvent的事件分发、处理机制
目录
三、为什么在前面经过测试的其他界面中,软键盘却又可以正常录入字符呢?
一、实现一个可以模拟输入的软键盘
一开始,我们的需求是在用户经常使用的部分界面中,增加虚拟软键盘,减少用户对于外接键盘的依赖
如图,在整单改价界面右侧增加了方便用户快捷输入的软键盘,用户不需要使用外接键盘,即可完成常见的商品改价等操作。
那么这个代码逻辑实现起来比较简单,因为业务中有许多类似界面需要使用该软键盘功能,所以我们将它单独封装为一个View:
mView = View.inflate(context, R.layout.res_keypad_view, this)
val map = hashMapOf<View, Int>()
map[tv_num_1] = KeyEvent.KEYCODE_NUMPAD_1
map[tv_num_2] = KeyEvent.KEYCODE_NUMPAD_2
map[tv_num_3] = KeyEvent.KEYCODE_NUMPAD_3
map[tv_num_4] = KeyEvent.KEYCODE_NUMPAD_4
map[tv_num_5] = KeyEvent.KEYCODE_NUMPAD_5
map[tv_num_6] = KeyEvent.KEYCODE_NUMPAD_6
map[tv_num_7] = KeyEvent.KEYCODE_NUMPAD_7
map[tv_num_8] = KeyEvent.KEYCODE_NUMPAD_8
map[tv_num_9] = KeyEvent.KEYCODE_NUMPAD_9
map[tv_num_0] = KeyEvent.KEYCODE_NUMPAD_0
map[tv_num_dot] = KeyEvent.KEYCODE_NUMPAD_DOT
map[tv_num_del] = KeyEvent.KEYCODE_DEL
map.iterator().forEach { item ->
item.key.setOnClickListener {
(mView.parent as View).dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, item.value))
}
}
做好布局后,在代码中建立每个view与它所代表的键码的对应关系,使用map持有它们,最后遍历map集合,给每个view设置点击事件,触发点击事件时,我们构造一个KeyEvent对象,然后找到当前view(即软键盘view)的父view,调用其dispatchKeyEvent()方法,向其分发键盘事件,然后依靠安卓自身的事件处理机制,该事件就能被正确的传递给需要它的EditText。
利用系统自身的事件传递机制,去帮我们实现将KeyEvent转化为字符,输入进EditText,是再好不过的,我们就不需要考虑直接操作EditText可能带来的各种问题。
那么,软键盘view封装好了,其他界面如何使用呢?
非常简单:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="400dp"
android:layout_height="350dp"
android:orientation="vertical">
<!-- 此处为原本的业务View -->
</LinearLayout>
<.....KeypadView
android:layout_width="263dp"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:background="@drawable/res_best_white_button_default" />
</LinearLayout>
只需要一个LinearLayout 将原本的业务view和软键盘view包括起来就OK了
好的,那么正当我测试了几个界面没有问题的时候,我把这个软键盘应用在另一个界面时,问题出现了:
二、问题:点击软键盘,没有任何反应,输入框没有填入字符
就是这个界面,我没有发现它与正常界面有何区别,于是只能尝试通过底层源码,来寻找问题原因
那么我首先怀疑是因为dispatchKeyEvent()方法没有正确将按键事件传递给EditText,但是经过调试后,发现事件确实有传递过来
首先,当我们通过debug模式,查看安卓源码时,记得打开源文件后,在右上角选择和你运行的机器对应的安卓sdk版本,比如我的真机,安卓版本是5.1.1,那么此处我要选择查看api 22 版本的源代码。这样就不会出现断点乱跳,无法定位代码的问题。
断点之后,通过代码逻辑可知,返回0表示没有处理/消耗此事件,那么问题很有可能出现在这个doKeyDown()方法内
在doKeyDown()方法内部,我发现了可疑代码,这段代码判断了,当KeyEvent的按键动作是按下时,使用了类似于数据库事务操作的方式,对其进行编辑
并返回一个布尔值,如果为true,则返回到外部:1,表示该keyEvent被处理/消耗。
继续深入该方法,发现KeyListener是一个接口,拥有好几个实现类
鼠标停留在变量上,可以看到此时,实现类是TextKeyListener
跟踪TextKeyListener的onKeyDown方法,最终发现,它实际调用的是QwertyKeyListener的onKeyDown()方法
此时,我发现了一行关键的代码,当我使用小键盘的1的code(KEYCODE_NUMPAD_1),和大键盘的1的code(KEYCODE_1),得到了截然不同的结果
接着看KeyEvent的getUnicodeChar()方法
/**
* Gets the Unicode character generated by the specified key and meta
* key state combination.
* <p>
* Returns the Unicode character that the specified key would produce
* when the specified meta bits (see {@link MetaKeyKeyListener})
* were active.
* </p><p>
* Returns 0 if the key is not one that is used to type Unicode
* characters.
* </p><p>
* If the return value has bit {@link KeyCharacterMap#COMBINING_ACCENT} set, the
* key is a "dead key" that should be combined with another to
* actually produce a character -- see {@link KeyCharacterMap#getDeadChar} --
* after masking with {@link KeyCharacterMap#COMBINING_ACCENT_MASK}.
* </p>
*
* @param metaState The meta key modifier state.
* @return The associated character or combining accent, or 0 if none.
*/
public int getUnicodeChar(int metaState) {
return getKeyCharacterMap().get(mKeyCode, metaState);
}
原因:传入小键盘键码,和大键盘键码,得到的结果不一致
根据注释,可以得知该方法即为将按键事件,转为字符的核心方法,根据keyCode和键盘控制键(如Ctrl,Shift,NumLock等),获得一个字符
当我们传入小键盘的键码时,无法正确获得对应字符,当传入大键盘的键码时,却可以获得字符,这就是问题的原因。
那么,下一个问题来了,
三、为什么在前面经过测试的其他界面中,软键盘却又可以正常录入字符呢?
那么,我们找到可以正常使用软键盘的界面,再重复一遍以上过程,
发现这次,代码没有进入TextKeyListener和QwertyKeyListener,而是进入了另一个实现类,NumberKeyListener的onKeyDown方法
可以发现,此处的区别是,它并没有用上面的getUnicodeChar()方法去转换字符,而是通过自己的lookup()方法,转换字符
原因:使用了不同的KeyListener实现类
那么,结合类名和现象,我们可以推测出:
TextKeyListener和QwertyKeyListener,他们的字符转换功能,不支持小键盘的键码(即虚拟键盘失效,有问题的界面)
NumberKeyListener的字符转换功能,支持小键盘键码(即前面测试过,功能ok的界面)
找到原因后,修复的办法也显而易见,只要让我们存在问题的界面内的EditText,使用NumberKeyListener做字符转换即可
使用EditText的 setInputType(EditorInfo.TYPE_CLASS_NUMBER)
将其设置为只能输入数字即可
但是,突然又想到,这个界面是需要支持输入商品编码的,而我们的部分商品编码是以字母Z开头的,所以设置为只能输入数字,将会影响业务逻辑
此时,看到了最开始我们发射事件的代码,
map[tv_num_1] = KeyEvent.KEYCODE_NUMPAD_1
map[tv_num_2] = KeyEvent.KEYCODE_NUMPAD_2
map[tv_num_3] = KeyEvent.KEYCODE_NUMPAD_3
map[tv_num_4] = KeyEvent.KEYCODE_NUMPAD_4
map[tv_num_5] = KeyEvent.KEYCODE_NUMPAD_5
map[tv_num_6] = KeyEvent.KEYCODE_NUMPAD_6
map[tv_num_7] = KeyEvent.KEYCODE_NUMPAD_7
map[tv_num_8] = KeyEvent.KEYCODE_NUMPAD_8
map[tv_num_9] = KeyEvent.KEYCODE_NUMPAD_9
map[tv_num_0] = KeyEvent.KEYCODE_NUMPAD_0
map[tv_num_dot] = KeyEvent.KEYCODE_NUMPAD_DOT
里面的键码全部是用的小键盘键码,直接把它们都改为大键盘键码,问题不就都迎刃而解了吗
改来改去,最后发现问题的原因只是最初发射事件的源头。。
哈哈,最后针对安卓中的键盘事件处理机制,做一下总结
安卓KeyEvent的处理机制总结:
- 如果需要做虚拟键盘,模拟键盘输入时,可以借助安卓本身的KeyEvent机制,自己构造KeyEvent对象,使用dispatchKeyEvent()方法将事件分发给父View,剩下的都可以交给系统自行处理~
- 如果出现事件无效的问题,检查自己构造的键码是否正确,切换大/小键盘键码进行尝试
- 将KeyEvent事件转换为字符输入的工作,是由KeyListener的实现类完成的
- KeyListener有很多实现类,每个实现类的职责不同,我们可以通过EditText的setInputType()方法,选择不同的实现类,来实现最终目的
本文地址:https://blog.csdn.net/kaixuan_dashen/article/details/112261931