触摸事件机制是Android触摸屏触控操作的关键。
触摸事件分发分为服务端和应用端。在Server端由WindowManagerService(WMS,窗口管理服务)负责管理。在Client端则是由ViewRootImpl(负责控制View树的UI绘制和事件消息的分发)负责分发的。
Touch事件从WMS经过一系列方法到ViewRootImpl进行传递,当Touch事件传递到了ViewRootImpl中后,就会在Activity的View树中进行分发。
WMS至ViewRootImpl
上面的图是网上找的,Android4.4后,InputManager变成了InputManagerService,ViewRoot变成了ViewRootImpl。
大致流程就是:WMS在启动之后,会在native层启动两个线程:InputReaderThread和InputDispatchThread,前者用来读取输入事件,后者用来分发输入事件,输入事件经过nativie层的层层传递,最终会传递到java层的ViewRootImpl中。
Touch事件从server端传递到client端采用的IPC方式并不是Binder,而是共享内存和管道,至于为什么不采用Binder,应该是共享内存的效率更高,而管道(注意,两个管道,分别负责不同方向的读和写)只负责通知是否有事件发生,传递的只是一个很简单的字符串,因此并不会太多地影响到IPC的效率。
ViewRootImpl至Activity
ViewRootImpl -> DecorView -> PhoneWindow -> Activity
ViewRootImpl事件分发
事件传递到ViewRootImpl后,会调用dispatchInputEvent方法。
1 | public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { |
- InputEvent:输入事件的基类,它有两子类:KeyEvent(键盘输入事件),MotionEvent(屏幕触摸事件)。
- InputEventReceiver:为应用程序提供一个接收者来接收输入事件。也就是用来接收输入事件->交给ViewRootImpl的dispatchInputEvent去分发。
接下来会发送MSG_DISPATCH_INPUT_EVENT事件到ViewRootHandler,ViewRootHandler在UI线程中。
1 | case MSG_DISPATCH_INPUT_EVENT: { |
接下来看看enqueueInputEvent方法。输入事件会以队列方式按顺序等待执行。
1 | void enqueueInputEvent(InputEvent event, |
异步情况下,MSG_PROCESS_INPUT_EVENTS事件发送到ViewRootHandler,最终也是调用了doProcessInputEvents方法。
1 | private void scheduleProcessInputEvents() { |
接下来看看doProcessInputEvents方法。
1 | void doProcessInputEvents() { |
1 | private void deliverInputEvent(QueuedInputEvent q) { |
这里InputStage是一个基类,表示事件处理的阶段。当一个InputEvent到来时,ViewRootImpl会寻找合适它的InputStage来处理,每个InputEvent对应一个InputStage。InputStage也采用了类似责任链的模式处理,InputStage主要有以下几种状态:
- deliver:交付事件阶段处理。
- forward:处理下个阶段。
- finish:标志当前输入事件处理完毕。
- apply:根据onProcess返回的RESULT_CODE判断当前阶段。
1 | public final void deliver(QueuedInputEvent q) { |
最终的事件分发处理则是在apply方法里的onProcess方法。
对于点击事件来说,InputState的子类ViewPostImeInputStage可以处理它。
1 | protected int onProcess(QueuedInputEvent q) { |
此处mView即为DecorView,dispatchPointerEvent方法在View中:
1 | public final boolean dispatchPointerEvent(MotionEvent event) { |
DecorView重写了dispatchTouchEvent:
1 | Override |
如果有Callback则用Callback的dispatchTouchEvent(ev)否则直接使用super.dispatchTouchEvent(ev),而此Window.CallBack由Activity实现。
1 | public class Activity extends ContextThemeWrapper implements Window.Callback { |
Activity至View
Activity -> PhoneWindow -> DecorView -> ViewGroup -> View
Activtiy分发流程
当触摸事件传递到Activity,Activity的dispatchTouchEvent()就会被调用:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
如果当前事件是DOWN事件,调用了onUserInteraction方法,该方法是一个空方法,我们可以重载该方法,在DOWN事件做一些处理。接着就把事件传递给Window来处理该事件。如果返回true,表示有View处理该事件,onTouchEvent方法返回了true,整个事件处理完成。否则Activity的onTouchEvent方法就会被调用。
Window分发流程
Window类是abstract的,唯一的具体实现类是PhoneWindow类,PhoneWindow的superDispatchTouchEvent:
1 | public boolean superDispatchTouchEvent(MotionEvent event) { |
即调用了DecorView的superDispatchTouchEvent方法。
DecorView分发流程
1 | public boolean superDispatchTouchEvent(MotionEvent event) { |
DecorView继承于FrameLayout,FrameLayout继承于ViewGroup。因此就是调用了ViewGroup的dispatchTouchEvent方法。
DecorView就是我们的顶层View,当我们通过setContentView方法设置的是顶层View的一个id为content的子View。
此后,事件在自定义的View树中进行分发。
触摸事件
先简述一下View的触摸事件,主要包含以下三个方法:
dispatchTouchEvent:用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和View的dispatchTouchEvent方法的影响,表示是否消费当前事件;
onInterceptTouchEvent:用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件;
onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消费当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
View树事件分发流程
View树事件分发使用了责任链方式处理,具体流程简略如下(截取自《Android开发艺术探索》):
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
事件由父View传递给子View,ViewGroup可以通过onInterceptTouchEvent()方法对事件拦截,停止其向子view传递。默认ViewGroup不对事件进行拦截。
如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会反向往上传递,这时父View可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouchEvent()函数。
如果当前View拦截事件,则自己进行onTouchEven处理,否则交由子View的dispatchTouchEvent处理,子View递归此过程。一旦触摸事件被消费,则后续事件序列不再进行调用。
ViewGroup和View的区别在于,View没有onInterceptTouchEvent事件,无法对事件直接进行拦截。但是子View可通过parentView.requestDisallowInterceptTouchEvent()对父视图调用表明父View不应使用onInterceptTouchEvent()截获触摸事件。
触摸事件的MotionEvent
View触摸事件的MotionEvent主要包括了ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL,ViewGroup还包含了POINTER_UP、POINTER_DOWN等事件。
同一个事件序列指的是从手指触摸屏幕的那一刻开始,到手指离开屏幕的那一刻结束,在这个过程产生的一系列事件。以ACTION_DOWN事件开始,可能经过多个ACTION_MOVE事件,最终以ACTION_UP事件结束。
某个View一旦开始处理事件,如果不消费掉ACTION_DOWN,那个这个事件系列的其他事件也不会再交与他处理,而是传给父容器的onTouchEvent()进行处理。
如果View不消费除ACTION_DOWN以外的事件,父容器的onTouchEvent()并不会被调用,这个View也可以继续收到其他事件,然后重新传递给了Activity进行处理。
onTouch()、onClick()、onTouchEvent()关系
- onTouch:OnTouchListener回调方法,监听触摸事件处理。
- onClick:OnClickListener回调方法,监听点击事件处理。
- onTouchEvent:处理触摸事件。
调用顺序:onTouch在View的dispatchTouchEvent中先调用,onTouchEvent后调用,onClick在onTouchEvent的ACTION_UP的performClick方法中调用。
如果onTouch返回true, onTouchEvent就不会调用,参考View的dispatchTouchEvent的方法:
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
clickable、enable关系
- clickable:表示控件是可点击的。
- enable:表示控件是可用的。
setClickable和setEnabled都是对mPrivateFlags做操作,判断View是否可点击以及是否可用也是基于mPrivateFlags做判断。
当mPrivateFlags & CLICKABLE == CLICKABLE时代表控件是可点击的。
当mViewFlags & ENABLED_MASK == ENABLED代表控件是可用的。
在View中对点击状态的判断主要是在onTouchEvent方法中,而对可用状态的判断则分布在 onTouchEvent和dispatchTouchEvent中。
当View是不可用的时候,通过setOnTouchListener设置的OnTouchListener中的onTouch方法将不会执行。
当View是不可用的时候,onTouchEvent会被执行,但不会执行实质的逻辑,比如onClick、onLongClick等方法不会被执行到。此时onTouchEvent的返回值由该View能不能点击(包括长按和短按等点击状态)来决定。可以点击时返回true,否则返回false。
当View是不可点击的时候,除非调用过View的setTouchDelegate方法传入 mTouchDelegate,否则onTouchEvent必定会返回false,具体的逻辑,例如onClick、onLongClick等不会被调用。
android:focusable和android:focusInTouchMode区别
- focusable:针对在键盘下操作的情况,比如非触屏手机或者TV,如果设置为true,则键盘上下左右选中,焦点会随之移动。
- focusableInTouchMode:针对触屏情况下的,也就是我们点击屏幕的上的某个控件时,不要立即执行相应的点击逻辑,而是先显示焦点(即控件被选中),再点击才执行逻辑。
android:focusable=“true”不会改变android:focusableInTouchMode,因此只在键盘状态下显示焦点,在TouchMode状态下,依旧无法显示焦点。
android:focusable=“false”,一定会使android:focusableInTouchMode=“false”。