下面进入正题,先来看下Android中事件的分类:
1、键盘事件:主要是指按下虚拟键盘的某个按键、或者机身的物理按键时产生的事件。
2、鼠标事件:Android4.0之后增加了对鼠标事件的监控,如ACTION_HOVER_ENTER。
3、触摸屏事件:凡是触摸屏幕而产生的事件都是触摸屏事件,触摸屏事件包括很多,比如单点触控、多点触控)、轨迹球事件等。
我们这里主要讲解单点触控事件,也就是Touch事件的传递,首先看下Touch事件的完整传递过程:
1、首先需要明白,Android中,Touch事件的分发分服务端和应用端。在Server端由WindowManagerService(WMS,窗口管理服务,不懂的自行脑补)负责采集和分发的,在client端则是由ViewRootImpl(内部有个mView变量指向View树的根 ,负责控制View树的UI绘制和事件消息的分发)负责分发的。
2、WMS在启动之后,经过逐层调用,会在native层启动两个线程:InputReaderThread和InputDispatchThread,前者用来读取输入事件,
后者用来分发输入事件,输入事件经过nativie层的层层传递,最终会传递到java层的ViewRootImpl中,调用
ViewPostImeInputStage(ViewRootImpl的内部类)中的各个方法来分发不同的事件,而Touch事件是在processPointerEvent方法进行分发的(这部分代码很单,可自行查看)。
3、processPointerEvent方法中调用mView.dispatchPointerEvent(event)方法,这里的mView就是在创建窗口后通过调用root.setView传进
来的DecorView,而dispatchPointerEvent方法会对event进行判断,如果是Touch事件的话,就调用dispatchTouchEvent将该事件分发DecorView,这样,Touch事件就传递到了View树中了。
Touch事件从WMS到ViewRootImpl的传递
下面这张图(不是自己画的,网上找的,Android4.4中,InputManager变成了InputManagerService,ViewRoot变成了ViewRootImpl)展示了Touch事件
从WMS(sever端)传递到ViewRootImpl(client端)的流程。
这里需要特别注意的是,Touch事件从server端传递到client端采用的IPC方式并不是Binder,而是共享内存和管道,至于为什么不采用Binder,应该是共享内存的效率更高,而管道(注意,两个管道,分别负责不同方向的读和写)只负责通知是否有事件发生,传递的只是一个很简单的字符串,因此并不会太多地影响到IPC的效率。
上图中,只有WMS、ViewRootImpl、InputManagerService、InputQueue是在FrameWork层实现的,其他部分都是在native层实现的,native 层的代码没有细看,参考了些网上的一些资料和公司内部的一些分享,把整个流程串通了,这部分代码,如果时间充足,可以深入研究下。
在sever端中,InputReader和InputDispatcher是native 层的两个线程,前者不断地从EventHub中读取事件(包括所有的事件,对不同的事件会做判断处理),后者则不断地分发InputReader读取到的事件。而实际的分发操作时在InputPublish中进行的,它里面保存的有一个指向server端InputChannel端的指针,和一个指向ShareMemory(共享内存)的指针,当有事件要分发时,它会将事件写入到ShareMemory中,并且传递一个特定的字符串给InputChannel,由inutChannel将该字符串写入到管道中。在client端,一旦InputChannel从管道中读取到有事件分发过来,便会通知InPutConsumer从ShareMemory中读取具体的事件,并传递到framework层的InputQueue中。同样,一旦事件消费完毕,client端会通过管道告诉server端,事件已经消费完毕,流程与上面的似。
大致的流程就是这样。
另外,顺便说下这里的两个InputChannel,这两个InputChannel是在native 层的,他们在framework层各自有一个对应的InputChannel类,对于这两个framework层的InputChannel,client端的是在ViewRootImpl中的setView中new出来的,但是并未做任何初始化操作(真正的初始化操作是跟server端的一起在WMS中执行的),也就是构造方法里面为空。
server端的InputChannel虽然是在server端创建的,但其创建过程是在client端发起的,ViewRootImpl中有server端的Session的代理,同样是setview方法中,通过Session代理执行server端Session的addToDisplay方法,该方法接受了client端的InputChannel方法
addToDisplay方法在Session类中,它会调用WindowManagerService的addWindow方法,而两个InputChannel的初始化操作都是在这里面的这这段代码中进行的。
这里的InputChannel.openInputChannelPair方法会在native层创建两个InputChannel,也就是我们上面看到的那两个,并返回对应的framework层InputChannel的两个对象,保存在iputChannels数组中,其中一个保留字server端,一个通过transferTo方法返回到client 端。这里的InputChannel.openInputChannelPair方法和transferTo方法中都是直接调用来了native方法,这里不再贴代码。
说了这么多,其实就是一个Binder机制,关于Binder机制,大家自行在搜索吧,入门的资料还是挺多的。这里我根据源码画了两份ViewRootImpl和WMS之间通过Binder 机制进行IPC的序列图,有兴趣的可以自行研究下代码,只要能搞清楚Binder机制,这部分代码还就不难懂。
ViewRootImpl到WMS的连接,通过WMS提供给ViewRootImpl的IWinowSession成员,也就是Session在本地的代理来完成:
WMS到 ViewRootImpl的连接,通过ViewRootImpl提供给WMS的Iwindow(ViewRootImpl的内部类)来完成:
Touch事件在View树中的分发
当Touch事件传递到了ViewRootImpl中后,就会在View树中进行分发,要了解Touch事件在View树中的分发,首先需要了解View树的创建,这部分又可以写成一篇单独的博文了,具体的过程这里不再详说,有兴趣的可以自己研究下。View树创建完成后的结构是这样的(图片源自网络):
View树的根View永远是DecorView,它继承自FrameLyout,其内部会有一个LinearLayout,根据Window Feather的的不同,LinearLayout内部的布局也不同,其中每种不同布局的xml(系统资源内部的xml布局)内都有一个id为content的FrameLayout,这就是我们在自己的布局所attach的父容器。
Touch事件的传递自然是先从ViewRootImpl传递到DecorView中,这个前面的第三点也说到了,因此我们这里就从DecorView入手,开始分析Touch事件的分发。
在开始分析之前,先大致梳理下Touch事件传递可能涉及到的一些基础:
1、一般情况下,每一个Touch事件,总是以ACTION_DOWN事件开始,中间穿插着一些ACTION_MOVE事件(取决于是否有手势的移动),然后以ACTION_UP事件结束,中间还会会有onTouch、Click、LongClick等事件。
2、事件分发过程中,包括对MotionEvent事件的三种处理操作:
分发操作:dispatchTouchEvent方法,后面两个方法都是在该方法中被调用的。
拦截操作:onInterceptTouchEvent方法(ViewGroup)
消费操作:onTouchEvent方法和OnTouchListener的onTouch方法,其中onTouch的优先级高于onTouchEvent,若onTouch返回true,那么就不会调用onTouchEvent方法。
3、dispatchTouchEvent分发Touch事件是自顶向下,而onTouchEvent消费事件时自底向上,onTouchEvent和onIntercepteTouchEvent都是在dispatchTouchEvent
中被调用的。
下面,正式进入对Touch事件在View树中分发的分析:
首先来看DecorView(PhoneWindow的内部类)中dispatchTouchEvent方法:
这里的cb就是当前的Activity,Activity实现了Window.Callback接口,同时在Activity的attach方法中,创建PhoneWindow后,调用了
mWindow.setCallback(this)将PhoneWindow中的callback设置为当前的的Activity,因此这里cb.dispatchTouchEvent就是Activity的
dispatchTouchEvent方法,如果前面三个条件同时成立(一般是都成立的),则调用Activity的dispatchTouchEvent方法进行事件的分发,
否则,直接调用super.dispatchTouchEvent方法,也即是FrameLayout的dispatchTouchEvent方法,其实即使调用了Activity的
dispatchTouchEvent方法,最终也是会调用到super.dispatchTouchEvent,我们可以继续往下看Activity的dispatchTouchEvent方法:
前面if分支不用管,这里会调用PhoneWindow的superDispatchTouchEvent方法,进去看看:
调用了DecorView的superDispatchTouchEvent方法,再进去看看:
最终还是调用来DecorView的super.dispatchTouchEvent,也就是说,无论怎样,DecorView的dispatchTouchEvent最终都会调用到自己父亲FrameLayout的dispatchTouchEvent方法,而我们在FrameLayout中找不到dispatchTouchEvent方法,所以,会去执行ViewGroup的
dispatchTouchEvent方法。如果该dispatchTouchEvent返回true,说明后面有view消费掉了该事件,那就返回true,不会再去执行自身的onTouchEvent方法,否则,说明没有view消费掉该事件,会一路回传到Activity中,然后调用自己的onTouchEvent方法,该方法的实现比较简单,如下:
这里的大致意思是,如果设置了mCloseOnTouchOutside属性为true(对应xml中的android:windowCloseOnTouchOutside属性),且当前事件为down事件,且down事件发生在该Activity范围之外,并且DecorView不为null,就返回true,很明显,dialog形的Activity可能会发生这种情况。
下面需要重点来看下ViewGroup中的dispatchTouchEvent方法了:
另外,画了张ViewGroup中dispatchTouchEvent方法代码执行的流程图,可以有助于大家对代码整体逻辑的把握(Windows上viso中画的图,传到mac上就变成这样了,重新保存成图片,清晰度太低,直接在PPT里面截出来了,凑合着看吧,没太大影响)。
关于上面提到的dispatchTransformedTouchEvent方法,这里就不多分析了,感兴趣可以自己分析下,另外,ViewGroup中没有复写onTouchEvent方法。
下面重点看下View中的dispatchTouchEvent方法。
很明显,会先判断该View有没有绑定OnTouchListener监听器,如果绑定了,并且复写了其中的onTouch方法,如果onTouch方法返回了true,那么Touch事件就被消费掉了,后面的onTouchEvent方法就不会得到执行,而如果没有被消费掉,才会执行到onTouchEvent方法,根据其返回值来判定Touch时间是否被消费掉。这里重点关注消费Touch事件的先后顺序:onTouch先于onTouchEvent。
下面就关键来看下View中的onTouchEvent方法了。
这里其实没太多要说的,重点关注:
1、onClick和onLongClick执行的时机:onClick时在UP事件中执行的,onLongClick实在Down事件中执行的,只是如果在Down事件中已经执行了onLongClick的话,则mHasPerformedLongPress变量会被置为true,这样在UP事件中,就会把onClick的回调remove掉,就不会再执行onClick了。
2、只要该View是clickable的,就一定会消费掉Touch事件,只是,如果该View是Disable的话,虽然消费掉了Touch事件,但是不做任何处理。
另外,有一点大致说下:
源码的前面部分有一个mTouchDelegate变量(默认为null),如果它不为null的话,会将Touch事件分发给它。具体的意思是这样的,假设有两个视图v1和touchDelegate1,它们的布局相互之间不重叠;如果设置了v1.setTouchDelegate(touchDelegate1)的话,v1的触摸事件就会分发给touchDelegate1中的view(TouchDelegate中有一个view变量)。
为了便于整体上对源码流程的把握,这里同样画了一个流程图
最后,关于整个Touch事件在View树中的传递流程,同样画了张流程图,看起来会更直观,有助于对整体流程的把控:
以上流程图中有些地方文字有错位,应该不影响对流程的整体理解和把握。
其实相对来说,事件的分发处理属于Android中比较基础的知识点,但想把整个流程完整地串通,还是要花些时间的,这篇文章在10月份的时候就想写了,但是工作后,写博客的时间越来越少,人也变得越来越懒了。。。整篇文章断断续续坚持着写下来还是挺费劲的。