平台:android2.2
场景:全键盘电子书项目需要为系统新增需要按键,同时对于一些touch事件需要做特殊的处理,所以需要对整个input进行一些了解。
时间:2011.12~2012.3

1.为系统添加新的硬件按键。(http://www.eefocus.com/chongzi865458/blog/11-06/225120_23131.html
物理键盘的按键响应,能够被上层应用所接收到,主要涉及到以下几个文件:
(1).system/usr/keylayout/xxx-keypad.kl(qwerty.kl)——与扫描码(kernel上报)相关的按键映射文件(将kernel上报的扫描码转换成字符串,同时带有flag信息),通过KeyLayoutMap.cpp进行解析,然后在EventHub.cpp调用(KeyLayoutMap的map()方法),按照映射文件转换成相应的键值并将扫描码和键码返回给KeyInputQueue。

(2).frameworks/base/include/ui/KeycodeLabels.h——在EventHub.cpp中使用。将qwerty.kl被转换出来的字符串,再次进行转换成int,与KeyEvent.java中的按键码一致。

KeycodeLabels.h中还对qwerty.kl中的flag信息进行了转换。Flag信息如下: 
 { “WAKE”, 0x00000001 }, // 可以唤醒睡眠,并通知应用层 
 { “WAKE_DROPPED”, 0x00000002 }, // 可以唤醒睡眠,不通知应用层 
 { “SHIFT”, 0x00000004 }, // 自动附加SHIFT 
 { “CAPS_LOCK”, 0x00000008 }, // 自动附加CAPS_LOCK 
 { “ALT”, 0x00000010 }, // 自动附加ALT (3).frameworks/base/core/Java/android/view/KeyEvent.Java——Java框架的API。
(4).frameworks/base/core/res/res/values/attrs.xml——属性的资源文件,需要修改其中的name=”keycode”的attr。

ios input 完成 input k_java


如图所示,从kernel的扫描码,到上层的KeyEvent.java,一共经历了两次转换:kl文件—将扫描码转换成字符串;KeycodeLabels.h—将字符串转换成与KeyEvent一致的int按键码。

添加好4个文件后,就可以在应用中响应到相关的KeyCode。但是,如果在一个EditText中,想通过按下一个物理键盘按键“A”,直接将“A”显示到EditText中,这就还需要另外一个家伙的支持——kcm文件。
关于kcm文件,起作用就是用于将按键的码映射为文本可识别的字符串(例如,显示的标签等)。当然,kcm需要被解析,起对应的解析文件就是KeyCharacterMap.cpp。kcm文件在system/usr/keychars/xxxx-keypad.kcm.bin,因为其本身较大,因此使用makekcharmap工具转化成二进制的格式。其源文件的内容部分如下:
[type=QWERTY]

keycode display number base caps fn caps_fn

A ‘A’ ‘2’ ‘a’ ‘A’ ‘#’ 0x00
B ‘B’ ‘2’ ‘b’ ‘B’ ‘<’ 0x00
C ‘C’ ‘2’ ‘c’ ‘C’ ‘9’ 0x00E7
display 键盘上显示的字符(丝印)
number 一般不需要
base 不使用组合键默认 显示的字符
caps Shift + 按键 显示的字符
fn Alt + 按键 显示的字符
caps_fn Shift +Alt+按键 显示的字符

第一列是转换之前的按键码,第二列之后分别表示转换成为的显示内容(display),数字(number)等内容。这些转化的内容和KeyCharacterMap.h中定义的getDisplayLabel(),getNumber()等函数相对应。所以,当你新添加的按键,或者是系统原来的按键,需要有相关的字符显示的时候,可以修改kcm文件。

KeyCharacterMap是一个辅助的功能:由于按键码只是一个与UI无关整数,通常用程序对其进行捕获处理,然而如果将按键事件转换为用户可见的内容,就需要经过这个层次的转换了。

KeyCharacterMap需要从本地层传送到Java层,JNI的代码路径如下所示: 
 frameworks/base/core/jni/android_text_KeyCharacterMap.cpp 
 KeyCharacterMap.Java框架层次的代码如下所示: 
 frameworks/base/core/Java/android/view/KeyCharacterMap.Java 
 android.view.KeyCharacterMap类是Android平台的API可以在应用程序中使用这个类。 
 android.text.method中有各种Linstener,可以之间监听KeyCharacterMap相关的信息。 
 DigitsKeyListener NumberKeyListener TextKeyListener。

2.输入事件响应到上层的流程。
从下往上,依次为:

(1).frameworks/base/libs/ui/EventHub.cpp::getEvent() 
 该函数中有一个while(1)的循环,通过poll的方式进行处理。(2).frameworks/base/services/jnicom_android_server_KeyInputQueue.cpp::readEvent() 
 Jni函数,封装了getEven(),被java框架中的KeyInputQueue.java中的InputDeviceReader线程调用。(3).frameworks/base/services/java/com/android/server/KeyInputQueue.java


此类中有一个关键的线程InputDeviceReader,其run()方法中亦是通过while(true)的方式调用readEvent(),进行监听input事件,当然,其最终调用到getEvent()中poll()。此处通过InputDevice.java描述了input对象,同时监听处理了input设备的变化。并将调用其子类WMS中的KeyQ实现的preprocessEvent()方法进行预处理。
同时,在KeyInputQueue.java中存在一个getEvent()方法,将被WMS中的InputDispatcherThread线程调用,在其中通过mFirst.wait(end-now)进行了超时等待,在KeyInputQueue.java中的addLocked()中进行了mFirst.notify()。而addLocked()函数,正是在InputDeviceReader线程的preprocessEvent()方法后,有机会被进行调用。(即先监听event事件并处理,再确认是否需要dispatcher)

(4).frameworks/base/services/java/com/android/server/WindowManagerService.java
有三个重要的相关对象或线程,分别是:
1#.mQueue,其为KeyQ的对象,KeyQ继承于InputDeviceReader,其实现了关键的preprocessEvent()方法。在其中根据input事件的不同,继续了相关的处理,与mPolicy(PhoneWindowManager对象)进行了交互。
比如说,在case RawInputEvent.EV_KEY的情况下,调用了
mPolicy.interceptKeyTq(event, !screenIsOff),
即为对按键消息抛入到消息队列前的过滤处理。

2#.mInputThread,其为InputDispatcherThread线程的对象,被用来dispatcher输入事件。其run()方法中也是在while(true)的情况下,调用了KeyInputQueue.java中的getEvent()函数,等待着可以dispatcher的输入事件,然后根据输入事件的种类,分别调用dispatchKey(),dispatchPointer(),dispatchTrackball()等方法
。在这些方法中,将再次与PhoneWindowManager交互,调用其interceptKeyTi()函数,即为dispatcher前的过滤函数。

3#.PolicyThread线程,其即为PhoneWindowManager对象mPolicy所在的线程,拥有自己的消息队列和消息循环。

(5).EventHub.cpp中的getEvent()进入死循环,执行pollres = poll(mFDs, mFDCount, -1)等待设备结点处有数据写入(也就是有键按下),当有数据写入时执行下面的for循环,找出是哪个fd中有内容写入,并读出写入的数据res = read(mFDs[i].fd, &iev, sizeof(iev))。
这里只读出了TYPE和Scancode(),而不会有Keycode,硬件层只能向设备文件写入Scancode,而Keycode是上层要用的,由map得到。接下来执行err = mDevices[i]->layoutMap->map(iev.code, outKeycode, outFlags)map出Scancode对应的Keycode和Flags。

3.关于TouchEvent的上报到应用层的过程,以及实例演示解析。
当touch事件上报后,将调用dispatchTouchEvent()分发事件。先看ViewGroup类中的dispatchTouchEvent()函数,其大致过程,是将事件按照View的树形结构一直往里层传递,直到最里层的view获取到事件。查看View类中的dispatchTouchEvent()函数发现,若此时View上存在ouTouch的监听器,则调用它,否则调用view的onTouchEvent()方法。
从这个过程可以知道,默认情况下,View上面的最里层的子View处理touch事件的优先级最高,而activity最低。
同时,根据返回值来确定父view是否可以继续处理这个事件(onTouchEvent()函数返回true则表示此事件已经被处理掉)。
需要注意的是,在ViewGroup类中的dispatchTouchEvent()函数中,在往子View分发事件的时候,有一个关于onInterceptTouchEvent()函数判断。此处,即为打断事件分发函数!在同一个view上,先调用onInterceptTouchEvent(),若返回true,则表示此View接管了这次的事件,而不再将事件往子view上分发。

存在一个这个的问题:
有一个LinearLayout,里面有很多的child view,他问如何监听这个LinearLayout的Click事件?他的做法是:

setClickable(true);

    setOnClickListener(listener);

    最后他发现listener中的回调函数根本不会被调用。

因为存在一些Event,如:Click、LongPress、DoubleClick等。这些Event通常是人为的将“硬件触发的Event”封装得来的。例如Click Event就是使用了TouchEvent 中的MotionEvent.ACTION_UP和MotionEvent.ACTION_DOWN封装而来的。默认情况下,它们使得onTouchEvent()处理函数返回了true。因此,Linearlayout的孩子们一旦设置了自己的Click事件监听器,Linearlayout本身就拿不到事件了,因为它的孩子已经进行了处理。

4.关于KeyEvent的上报以及处理流程。

(1).interface—->KeyEvent.Callback::onKeyDown()。一般通过KeyEvent对象event.dispatch()进行调用;

(2).interface—->Window.Callback::dispatchKeyEvent();

(3).View::dispatchKeyEvent()。一般是其子类重载此函数。注意区分与Window.Callback::dispatchKeyEvent()的区别!

同时View实现了KeyEvent.Callback。

(4).PhoneWindow中的DecorView继承View,重载了View的dispatchKeyEvent()方法;

(5).Activity实现了Window.Callback和KeyEvent.Callback。因此,其子类亦可以重载onKeyDown()和dispatchKeyEvent()方法。

请看图:

ios input 完成 input k_java_02


在MyAcitivy中重载了dispatchKeyEvent()和OnKeyDown()方法,return调用super.xxx()。

为什么LOG显示先从DecorView开始dispatch?—ViewRoot的调用!WMS跨进程调用ViewRoot中的W类的相关方法,然后在ViewRoot::deliverKeyEvent通过mView.dispatchKeyEventPreIme(event)调用到DecorView里面!

ios input 完成 input k_ios input 完成_03


在MyAcitivy的dispatchKeyEvent()函数中直接return true。

ios input 完成 input k_Java_04


在MyAcitivy的OnKeyDown()函数中return true。

5.其他零碎知识点
(1).Event设备在用户空间大多使用read、ioctl、poll等文件系统的接口进行操作,read用于读取输入信息,ioctl用于获得和设置信息,poll调用可以进行用户空间的阻塞,当内核有按键等中断时,通过在中断中唤醒poll的内核实现,这样在用户空间的poll调用也可以返回。

(2).可以使用getevent对Event设备进行调试。

(3).在处理过程中,将搜索路径下面的所有Input驱动的设备节点,这在openPlatformInput()中通过调用scan_dir()来实现,scan_dir()将会从目录中查找设备,找到后调用open_device()将其打开。open_device()函数还将打开system/usr/keylayout/中的kl文件来处理。

(4).在手机系统中经常使用的键盘(keyboard)和小键盘(kaypad)属于按键设备EV_KEY,轨迹球属于相对设备EV_REL,触摸屏属于绝对设备EV_ABS。