前言
之前研究过View分发事件的源码,但是一段时间之后再来看这个知识点,发现都已经忘光光了,还得从头开始去查资料、学习。总结来说造成这种现象的原因有两个:
- 对View事件分发没有理解的很透彻。
- 技术是一种你不用,转瞬即逝的东西。
所以针对这种情况,我觉得也有两点对应的解决办法:
- 彻底的对这个知识点进行理解,然后写到自己的博客中,方便之后查阅。
- 要多写代码,自己去找一些好的项目做。多用才能印象深刻。
另外事件分发机制在面试的时候会经常被提及,掌握它可以给你加分不少。
MotionEvent事件初探
我们对屏幕的点击、滑动、抬起等一些列的动作都是由一个个MotionEvent对象组成的,根据不同的动作,主要有以下三种类型:
- ACTION_DOWN: 手指刚接触屏幕,按下去的那一瞬间产生该事件。
- ACTION_MOVE: 手指在屏幕上移动的时候产生该事件。
- ACTION_UP: 手指从屏幕上松开的瞬间产生该事件。
上面是我们非常熟悉的三种事件, 从ACTION_DOWN开始到ACTION_UP结束称为一个事件序列。
正常情况下,MotionEvent有两种类型的操作:
- 单击:ACTION_DOWN ——> ACTION_UP
- 点击后滑动,再抬起: ACTION_DOWN ———-> ACTION_MOVE ————-> ACTION_MOVE ————> ACTION_UP
此外在MotionEvent中还封装了一些其他信息,比如点击的坐标等。
MotionEvent事件分发
- 下面三个方法也是我们非常熟悉的,View的事件分发由这三个方法完成:
public boolean dispatchTouchEvent(MotionEvent event)
- 顾名思义,这个方法是进行事件分发的,如果一个MotionEvent传递给一个View,那么dispatchTouchEvent方法一定会被调用!
- 返回值:表示是否消费了当前事件。可能是当前view本身的onTouchEvent方法消费,也可能是子View(当前View是ViewGroup)的dispatchTouchEvent方法消费,返回true表示事件被消费,本次事件终止,返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法。
public boolean onInterceptTouchEvent(MotionEvent ev)
- 当一个ViewGroup在接到MotionEvent事件序列时,首先会调用此方法判断是否需要拦截(View没有拦截方法,只有ViewGroup有)
- 返回值:返回true表示拦截事件,那么事件不继续传递,而直接调用ViewGroup的onTouchEvent方法,返回false表示不拦截,那么事件就传递给子View的dispatchTouchEvent方法。
public boolean onTouchEvent(MotionEvent ev)
- 这个方法真正对MotionEvent进行消费和处理,它在dispatchTouchEvent方法中被调用。
- 返回值:返回True表示事件被消费,本次的事件终止,返回false表示事件没有被消费,将调用父View的OnTouchEvent方法。
View && ViewGroup事件分发的不同
- ViewGroup本身也是View(ViewGroup继承自View),它采用了组合模式,目的是为了方便将View和ViewGroup能统一起来进行操作,方便操作。
- View本身不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
- 用一张图来看下事件传递流程:
需要强调的点
- 子view可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外)而这就是我们处理滑动冲突常用的关键方法。
- 对于View&ViewGroup而言,如果设置了onTouchListener,那么onTouchListener方法中的onTouch会被回调,onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)所以三者的优先级是:onTouch > onTouchEvent > onClick
- View的onTouchEvent方法默认会消费掉事件(返回true),除非view是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,如Button的clickable默认为true,而TextView的clickable默认为false。
View事件分发源码
- 之前每次看事件分发这个知识点,都会看源码,源码这玩意,漫无目的的看,一会就蒙圈了,所以之前每次看,要花很长事件才能理解个大概,但是一段时间之后就又忘完了。。
- 下面的代码是参考网上某大神的讲解,只有关键代码,大家感兴趣的可以扒出来完整的代码对比查看。
Activity的dispatchTouchEvent方法
当我们手触摸手机屏幕的时候,最先获取事件的肯定是硬件,系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装称为一个MotionEvent对象,然后将该对象放到一个队列中,系统的另一个线程循环读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于栈顶的ACtivity。
由上面可知,点击事件会给Activity附属的Window进行分发,如果事件被消耗,那么返回true,如果返回false,表示事件分发下去没有View可以处理,则最后返回到Activity自己的onTouchEvent方法执行。
- 这里的Window其实是一个PhoneWindow,它继承自Window,里面包含DecorView是Android一个界面最顶层的View。
ViewGroup的事件传递—onInterceptTouchEvent
DecorView继承自FrameLayout,很显然是一个ViewGroup,之前提到过:事件到达View会调用dispatchTouchEvent方法,同时ViewGroup会先判断是否拦截该事件。
上面提到过,子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),从上面的代码我们能够找到原因,是因为当时ACTION_DOWN事件时,系统会重置ALLOW_INTERCEPT标志位并且将mFirstTouchTarget(事件由子view去处理时mFirstTouchTarget会被赋值并指向子view)设置为null。
- 当事件为ACTION_DOWN 或者mFirstTouchTarget != null(即事件由子View处理)时会进行拦截判断,判断依据是子view是否调用了requestDisallowInterceptTouchEvent方法。
- 如果事件不为ACTION_DOWN且mFirstTouchTarget != null(即事件由ViewGroup自己处理)那么事件本来是自己处理也没必要拦截了。
- 所以onInterceptTouchEvent并非每次都会调用,如果要处理所有的点击事件,需要用dispatchTouchEvent方法。
ViewGroup的事件传递—子View处理
- 当ViewGroup不拦截事件,那么事件交给子View处理
|
|
- 如果没有找到合适的子view去处理事件,即ViewGroup并没有子view或者子view虽然处理了事件,但是子view的dispatchTouchEvent返回了false(一般是子view的onTouchEvent返回false),那么ViewGroup自己处理这个事件。如下代码所示:
|
|
- 当child为null时,handled = super.dispatchTouchEvent(event); 即调用了View的dispatchTouchEvent方法,点击事件给了View,此致事件分发全部结束。
view.dispatchTouchEvent
- 从上可知,如果没有子View处理点击事件,最终会调用View.dispatchTouchEvent。下面是它的源码,我将关键点都写到注释中。
|
|
- 从上面代码可以看出,View会先判断是否设置了OnTouchListener,如果设置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不会被调用,当没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false则会调用View自己的onTouchEvent方法。
view的onTouchEvent方法
- 直接来看代码吧
|
|
- 从上面代码可知,即便View是disable状态,依然不会影响事件的消费,只是它看起来不可用。
- 另外只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件(我们上面提到过这个点)。即View的onTouchEvent方法默认都会消费掉事件(返回true),除非它是不可点击的。
- ACTION_UP方法中有performClick(),如果View设置了OnClickListener,那么会回调onClick方法。
- 最后强调一点,假如我们想让View默认不可点击,将View的Clickable设置为false,在合适的时候需要可点击所以我们又给View设置了OnClickListener,那么你发现View依然可以点击,也就是说setClickable失效了,这是因为在setOnClickListener的时候,系统会覆盖点击状态,将View的点击状态设置为true,所以应该在setOnclickListener之后SetClickable()。
总结
- 上面比较系统的讲了MotionEvent事件分发机制,可能一次看完有点吃不消,需要多次查看来逐步消化。
##