1.事件响应机制的预备知识
在深入了解Android事件响应机制前,一些预备知识我们应该有所了解。
1.1 onTouch是优先于onClick执行,事件传递的顺序是先经过onTouch,再传递到onClick。
1.2 Android中的事件onClick、onLongClick、onScroll等,都是由多个Touch事件(一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP)组成。
1.3 Android事件响应机制是“由外到内”分发、“由内到外”处理的形式实现的。
1.4 MotionEvent对象的四种状态
MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
1.5 消息宿主
Activity只有dispatchTouchEvent和onTouchEvent,没有onInterceptTouchEvent
ViewGroup三个函数都有
View只有dispatchTouchEvent和onTouchEvent,没有onInterceptTouchEvent
2.Android事件处理的三个重要函数
Android事件分发机制主要由“事件分发”—>“事件拦截”—>“事件响应”这三步来进行逻辑控制的。本文也将从这三步对应的函数来分析。
2.1 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
当监听到有触发事件时,首先由Activity进行捕获,然后事件就进入事件分发的流程。Activity本身没有事件拦截,从而将事件传递给最外层的View的dispatchTouchEvent(MotionEvent ev)方法,该方法将对事件进行分发。
return true : View消费所有事件。
return false :停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,那么事件将被系统消费、处理。
super.dispatchTouchEvent(ev): 将事件交由本层的事件拦截onInterceptTouchEvent方法处理。
2.2 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
return true: 对事件拦截,交由本层的onTouchEvent进行处理。
return false: 不拦截,分发到子View,由子View的dispatchTouchEvent方法处理。
super.onInterceptTouchEvent(ev):默认表示事件不拦截,分发给子控件。
2.3 事件响应:public boolean onTouchEvent(MotionEvent ev)
return true: 表示onTouchEvent处理完事件后消费了此次事件。
return false: 不响应事件,不断的传递给上层的onTouchEvent方法处理,直到某个View的onTouchEvent返回true,则认为该事件被消费。如果到最顶层View还是返回false,那么该事件不消费,将交由Activity的onTouchEvent进行处理。
return: super.onTouchEvent,不响应事件,结果与return返回false一样。
此时,就可以得出我们通常所说的两个方向:
1.事件传递的方向:父控件→子控件
2.事件响应的方向:子控件→父控件
结合上面的理解,我们再来看看Touch事件传递机制流程图
3.实例分析
理解事件传递的基本逻辑,对于工作过程中解决滑动事件冲突非常有帮助。比如我们此时有一个父控件ViewPager,这个ViewPager其中一个Item是ScrollView,此时会发生什么问题呢?当ViewPager滑动到ScrollView这个条目的时候,再左右滑动,发现ViewPager再也左右滑动不了了。这是为什么呢?
1.我们都知道ViewPager是能够横向滑动的控件,而ScrollView是纵向滑动的控件,当Down事件产生的时候,此时会由ViewPager传递给ScrollView,ViewPager没有对Down事件拦截,ScrollView也不会对这个Down事件进行拦截,所以事件就会传递给ScrollView的孩子,也就是类似于图6中的子View,子View如果没有对Down事件响应,那么最后会到ScrollView中的onTouchEvent,而ScrollView的onTouchEvent对于这个Down事件返回了true,代表ScrollView消费了这个Down事件。
2.接下来开始滑动手指,产生一系列的Move事件。Move事件也是由ViewPager传递给ScrollView。由于Down事件是被ScrollView的onTouchEvent中消费的,所以Move事件就不会传递给ScrollView的子控件了。一系列的Move事件也是在ScrollView的onTouchEvent中被执行。
3.最后的Up事件也是由ScrollView中的onTouchEvent消费。
从上述1至3的步骤中,我们看出来无论是Down事件、Move事件还是Up事件,最后全部都是被ScrollView所消费。从头到尾ViewPager的onTouchEvent都没有得到执行。而ViewPager之所以能够左右滑动,正是因为ViewPager的onTouchEvent里面的代码逻辑产生的效果。ViewPager的onTouchEvent没有执行,这个ViewPager当然就不能够左右滑动了。所以解决上述问题,就是在于如何让ViewPager中的onTouchEvent方法执行。
我们可以自定义一个MyViewPager继承ViewPager,重写onInterceptTouchEvent方法,如果我们在onInterceptTouchEvent方法中直接野蛮地return一个true,此时就代表无论是Down事件、Move事件,还是Up事件,全部都拦截下来了,拦截在MyViewPager中,既然拦截下来了所有事件,那么所有事件就会传递到MyViewPager的onTouchEvent,所以此时,这个MyViewPager一定可以左右滑动。
但是,由此会引发另外一个问题,就是这个ScrollView不能上下滑动了。这又是为什么呢?因为ScrollView能够上下滑动的代码逻辑在ScrollView中的onTouchEvent方法内,而此时事件又全部被MyViewPager拦截了下来,ScrollView完全得不到事件,onTouchEvent方法得不到执行,自然不能上下滑动。所以我们需要修改MyViewPager中的onInterceptTouchEvent的逻辑。
ViewPager只对左右滑动感兴趣,而ScrollView对上下滑动这个动作感兴趣,所以我们只需要在MyViewPager的onInterceptTouchEvent中,根据多个Move事件,判断是左右滑动还是上下滑动,如果是左右滑动,return true将事件拦截下来,如果是上下滑动,return false将事件传递给ScrollView,这样就能解决问题了。
所以,对于Down事件,我们一般都不进行拦截,判断是否拦截得根据一些列的Move事件才能得出具体的条件是否成立。
Cancel事件的产生
刚才我们说了事件一般有三个,Down、Move、Up,这三个事件比较好理解。其实还有一种事件就是Cancel事件。它代表什么含义呢?
如果一个Down事件产生了,这个Down事件从ViewGroupA传递到ViewGroupB,最终到达子View,被子View的onTouchEvent消费,return了true,那么此时Down事件就终止了。接下来后续的Move事件也会从ViewGroupA传递给ViewGroupB,也就是说ViewGroupA和ViewGroupB会比子View更先拿到Move事件,那既然ViewGroupA和ViewGroupB比子View更先拿到Move事件,那么他们当中的任何一个都有可能在某一个Move事件中,把这个Move事件给拦截下来,一旦Move事件被拦截下来了,子View肯定就拿不到这个Move事件了,不过,此时子View会产生一个新的事件,就是Cancel事件。
所以一个正常的事件序列是 Down→Move→Up,这样才被认为是一个正常的事件序列。如果一个View响应的Down事件,可是却被没有正常结尾,Move事件或者Up事件被拦截了,此时非正常结尾的情况就会给子View产生一个新的事件Cancel。
子控件可以影响父控件是否拦截的行为
子控件是可以干预父控件是否拦截事件的结果。通过在子View中dispatchTouchEvent中增加一行代码即可。getParent().requestDisallowInterceptTouchEvent(true);这行代码就可以请求父控件不要拦截事件。
很多人可能不太明白这句话的意思,既然事件一定是先到达父控件,然后才到达子View,那也就是getParent().requestDisallowInterceptTouchEvent(true);这句话是在父控件是否拦截判断结束之后才调用,怎么能改变父控件是否拦截的结果呢,这里存在一个执行先后顺序的疑惑。
其实是这样的,getParent().requestDisallowInterceptTouchEvent(true);达到的效果不是修改父控件对本次事件是否拦截的结果,而影响的是后续事件。比如子View在Down事件中调用了getParent().requestDisallowInterceptTouchEvent(true);这行代码,那么在后续Move事件、Up事件产生到达父控件的时候,父控件就不会再拦截了。所以getParent().requestDisallowInterceptTouchEvent(true);只会影响Move事件和Up事件,影响不到Down事件。
4. 总结
通过上面的叙述,相信大家对Android的分发机制有了初步的理解。为了加深大家的理解,下面做个简单的总结。
ViewGroup默认不拦截任何事件。
点击事件的分发过程如下:dispatchTouchEvent—>onTouchListener的OnTouch方法—>onTouchEvent—>onClickListener的onClick方法。从而也可以看出onTouch优先于onClick执行。
子View可以通过使用getParent().requestDisallowInterceptTouchEvent(true),阻止ViewGroup对其MOVE或UP事件进行拦截。
一个点击事件产生后,传递过程是:Activity—>Window—>View。顶级View接受到事件后,就会按照上面的规则去分发事件。