事件拦截机制分析
我们平时点击屏幕时候,屏幕会根据我们不同的操作,比如单点、滑动、双击等做出不同的响应事件,这个过程我们称之为屏幕触摸事件。Android为这些触摸事件封装了一个类——MotionEvent,如果重写onTouchEvent()方法,就会发现该方法里就是用的MotionEvent这个参数。我们可以看官方里是这样介绍这个类的:
* Object used to report movement (mouse, pen, finger, trackball) events. * Motion events may hold either absolute or relative movements and other data, * depending on the type of device.也就是说这个类主要用于反应出各种和触摸事件相关的内容,比如可以通过Motion Event.getX()或者MotionEvent.getRawX()方法取出横向的坐标点,可以根据MotionEvent.getAction()获取的值判断出是当前是触发了哪种触摸事件,然后写出对应的监听事件。
但是我们只有一个屏幕,而在Android中View是树形结构,屏幕中的一个位置可能对应多层View和ViewGroup,那么这个位置触发的触摸事件由哪层View或者ViewGroup来响应呢?此时我们就需要进行事件拦截。
首先我们先了解下Android处理点击事件的三个方法:
l dispatchTouchEvent(分发点击事件)
l onInterceptTouchEvent(拦截点击事件)
l onTouchEvent(处理点击事件)
因为点击事件的入口都是Acitivity,由它将点击事件传递给指定的View或者ViewGroup来响应,中间的流程可以用下面这个图来表示(感谢网友flueky的专栏整理的图):
由三色线分别表示返回值为true,false或者super时候点击事件的传递顺序。如果没有看懂这个图,那也没关系,我们可以写个demo来进行说明。
首先我们写了两个自定义ViewGroup和一个自定义View,将他们的这三个点击事件都输出Log日志,其中View里没有onInterceptTouchEvent,其实想想也是合理的,比较View里不会含有子View了,无需进行拦截。如下所示:
public class ViewGroup_A extends RelativeLayout { private static String tag = "ViewGroup_A"; public ViewGroup_A(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public ViewGroup_A(Context context, AttributeSet attrs) { super(context, attrs); } public ViewGroup_A(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.e(tag,"--dispatchTouchEvent--A"); return super.dispatchTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { Log.e(tag,"--onInterceptTouchEvent--A"); return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e(tag, "--onTouchEvent--A"); return super.onTouchEvent(event); } }B和A一样,然后是C:
public class View_C extends View { private static String tag = "View_C"; public View_C(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public View_C(Context context, AttributeSet attrs) { super(context, attrs); } public View_C(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.e(tag,"--dispatchTouchEvent--C"); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e(tag, "--onTouchEvent--C"); return super.onTouchEvent(event); } }布局文件如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.diit.blocktest.MainActivity"> <com.diit.blocktest.ViewGroup_A android:layout_width="match_parent" android:layout_height="match_parent"> <com.diit.blocktest.ViewGroup_B android:layout_width="match_parent" android:layout_height="match_parent"> <com.diit.blocktest.View_C android:layout_width="match_parent" android:layout_height="match_parent" /> </com.diit.blocktest.ViewGroup_B> </com.diit.blocktest.ViewGroup_A> </RelativeLayout>这样就形成了三个嵌套的布局。这时候我们点击下屏幕看看日志为:
06-05 22:03:00.880 21802-21802/com.diit.blocktest E/ViewGroup_A: --dispatchTouchEvent--A
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/ViewGroup_A: --onInterceptTouchEvent--A
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/ViewGroup_B: --dispatchTouchEvent--B
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/ViewGroup_B: --onInterceptTouchEvent--B
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/View_C: --dispatchTouchEvent--C
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/View_C: --onTouchEvent--C
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/ViewGroup_B: --onTouchEvent--B
06-05 22:03:00.881 21802-21802/com.diit.blocktest E/ViewGroup_A: --onTouchEvent--A
也就是整个流程中的,没有进行任何拦截事件,点击事件从A传到B再传到C,响应事件的顺序从C到B再进行A,按照这样的顺序进行。
在默认情况下,每个事件return的值是false,也就是不做任何拦截,会在响应后继续向下一步走。
此时我们修改下ViewGroup_A里的值,将onInterceptTouchEvent()的返回值改为true,看看情况如何呢?此时运行下,得到的日志为:
06-05 22:17:11.249 7276-7276/com.diit.blocktest E/ViewGroup_A: --dispatchTouchEvent--A
06-05 22:17:11.250 7276-7276/com.diit.blocktest E/ViewGroup_A: --onInterceptTouchEvent--A
06-05 22:17:11.250 7276-7276/com.diit.blocktest E/ViewGroup_A: --onTouchEvent--A
我们可以看到只是在A里进行了点击分发、拦截和响应事件,也就是说在A的onInterceptTouchEvent()方法后就被拦截了,不会再向A的子View传下去,在A里就对这个点击事件进行了处理。
我们继续做个试验,将A的onInterceptTouchEvent()方法返回值改回来,然后将C的onTouchEvent()方法的返回值改为true,此时得到的日志为:
06-05 22:26:07.922 16163-16163/com.diit.blocktest E/ViewGroup_A: --dispatchTouchEvent--A
06-05 22:26:07.922 16163-16163/com.diit.blocktest E/ViewGroup_A: --onInterceptTouchEvent--A
06-05 22:26:07.922 16163-16163/com.diit.blocktest E/ViewGroup_B: --dispatchTouchEvent--B
06-05 22:26:07.922 16163-16163/com.diit.blocktest E/ViewGroup_B: --onInterceptTouchEvent--B
06-05 22:26:07.922 16163-16163/com.diit.blocktest E/View_C: --dispatchTouchEvent--C
06-05 22:26:07.923 16163-16163/com.diit.blocktest E/View_C: --onTouchEvent--C
此时我们可以发现点击事件从A传到C以后,C进行了响应事件,但在响应后,并没有在它的上级再进行响应。
基于上面的试验,我们可以得到这个结论:
1. 正常情况下,屏幕的点击事件是由父View向子View一级级传递下去,到了最上层的View开始执行响应事件,而响应事件是由子View向父View一级级传递回去;
2. onInterceptTouchEvent()方法主要用来拦截点击事件向该ViewGroup的子View里进行传递,当返回值为true时候,点击事件在该ViewGroup里被拦截,然后开始执行该ViewGroup的响应事件,并一级级向上传回去;
3. onTouchEvent()方法里的返回值主要是用来拦截响应事件,当返回值为true时候,响应事件在该View或者ViewGroup里被拦截,不会再向其父View传递响应事件。
由流程图为: