1.点击事件的传递规则
点击事件的分发过程由3个方法完成,分别是:
- public boolean dispatchTouchEvent(MotionEvent event)
- public boolean onInterceptTouchEvent(MotionEvent event)
- public boolean onTouchEvent(MotionEvent event)
public boolean dispatchTouchEvent(MotionEvent event)
用来进行事件分发。如果事件能够传递给当前View,那么次方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。true:拦截 false:不拦截
什么是事件序列?一个事件序列是指从手指触摸屏幕开始,到手指离开屏幕结束,这个过程中产生的一系列事件。一个事件序列以ACTION_DOWN事件开始,中间可能经过若干个MOVE,以ACTION_UP事件结束。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,true:消耗,false:不消耗。如果不消耗,则在同一事件序列中,当前View无法再次接受到事件。
它们之间的关系可以用下面的伪代码表示:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
事件传递规则:
对于一个根ViewGroup来说,当发生点击事件时,首先会传递给自己,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true,就表示ViewGroup自己要拦截点击事件,而点击事件就会交由ViewGroup自己处理,这时ViewGroup的onTouchEvent就会调用。如果ViewGroup的onInterceptTouchEvent返回false,就表示ViewGroup不需要拦截点击事件,这时点击事件就会传递给子View,这时子View就会调用dispatchTouchEvent,如此反复直到事件被处理掉。
当一个View处理事件时,如果当前的View设置了OnTouchListener,那么OnTouchListener中的onTouch方法就会被调用。这时事件如何处理还要看onTouch的返回值,如果返回true那么View的onTouchEvent方法就不会被调用,如果返回false那么View的onTouchEvent方法还是会被调用的。
优先级的高低,从高到低分别是:onTouchListener–>onTouchEvent–>onClickListener
以下结论有助于理解View的分发机制
1.正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此用一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View。
2.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。
3.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
4.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
5.ViewGroup默认不拦截任何事件。
6.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
7.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的。View的longClickable属性都默认为false。
8.View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只有它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
9.onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
10.事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
2.事件的分发过程
当一个点击事件产生后,它的传递过程遵循如下的顺序:
Activity–>Window–>View
点击事件用MotionEvent来表示,当一个点击操作发生时,最先传递给Activity,再由Activity的dispatchTouchEvent来进行事件分发。先是由Activity内部的Window(实现类时PhoneWindow)将事件传给DecorView(这个DecorView就是setContentView的父容器),由于DecorView是继承自FrameLayout而且是父View(ViewGroup),最后在将事件传递给下面的子View。
下面以一个例子的方法说明事件的分发过程:
在Activity中创建一个ViewGroup(father),再在这个ViewGroup(father)中创建一个ViewGroup(child),最后在ViewGroup(child)中放一个ImageView,代码结构如下:
下面是具体的代码
Log.java
public class Log {
public static void d(String tag, String msg) {
android.util.Log.e(tag,msg);
}
public static void w(String tag, String msg) {
android.util.Log.w(tag, msg);
}
public static void e(String tag, String msg) {
android.util.Log.e(tag, msg);
}
public static void i(String tag, String msg) {
android.util.Log.i(tag, msg);
}
}
MyImageView.java
public class MyImageView extends ImageView {
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
android.util.Log.e("eventTest", "MyImageView | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent ev) {
android.util.Log.d("eventTest", "MyImageView | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onTouchEvent(ev);
}
}
TouchEventActivity.java
public class TouchEventActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("eventTest", "Activity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("eventTest", "Activity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));
return super.onTouchEvent(event);
}
}
TouchEventChilds.java
public class TouchEventChilds extends LinearLayout {
public TouchEventChilds(Context context) {
super(context);
}
public TouchEventChilds(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("eventTest", "Childs | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("eventTest", "Childs | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onInterceptTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent ev) {
android.util.Log.e("eventTest", "Childs | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onTouchEvent(ev);
}
}
TouchEventFather.java
public class TouchEventFather extends LinearLayout {
public TouchEventFather(Context context) {
super(context);
}
public TouchEventFather(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("eventTest", "Father | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("eventTest", "Father | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onInterceptTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent ev) {
Log.e("eventTest", "Father | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onTouchEvent(ev);
}
}
TouchEventUtil.java
public class TouchEventUtil {
public static String getTouchAction(int actionId) {
String actionName = "Unknow:id=" + actionId;
switch (actionId) {
case MotionEvent.ACTION_DOWN:
actionName = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
actionName = "ACTION_MOVE";
break;
case MotionEvent.ACTION_UP:
actionName = "ACTION_UP";
break;
case MotionEvent.ACTION_CANCEL:
actionName = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_OUTSIDE:
actionName = "ACTION_OUTSIDE";
break;
}
return actionName;
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.qqyumidi.event.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="320dp"
android:layout_height="480dp"
android:background="#468AD7"
android:gravity="center"
android:orientation="vertical">
<com.qqyumidi.event.TouchEventChilds
android:gravity="center"
android:id="@+id/childs"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:background="#66ff0000">
<com.qqyumidi.event.MyImageView
android:scaleType="fitXY"
android:background="#66ff0000"
android:layout_gravity="center"
android:layout_width="200dip"
android:layout_height="200dip"
android:src="@drawable/mm" />
</com.qqyumidi.event.TouchEventChilds>
</com.qqyumidi.event.TouchEventFather>
运行代码,效果如图
1.什么都不做,即每一个View和ViewGroup、Activity都是默认返回super.XXX方法时, 当点击中间的MyImageView时,打印的log如下:
点击MyImageView时,按理来说MyImageView应该消费了点击事件,但是从log中可以看出ACTION_DOWN并没有在MyImageView中消费掉,而是最终又传到了Activity中。接下来的ACTION_UP并没有传到Father和Childs中,而是直接交由Activity处理,这样印证了上面结论3。可以看出View默认在onTouchEvent中返回false,也就是View默认是不消费事件的。
这里要提一点的是,处理和消费的概念,它们并不是同一个意思。处理:比如说点击事件在OnTouchEvent中处理,只是回调了OnTouchEvent方法,事件不一定消费掉了。消费:事件已经消耗了,不会再继续传递下去。
2.当MyImageView的onTouchEvent返回true时,点击MyImageView,打印的log如下:
从log中可以看到点击事件的ACTION_DOWN在MyImageView中已经停止传递了,接下来的ACTION_UP也会传递到MyImageView中,并在MyImageView中消费掉。它与上面的子View不消费掉事件,最终交给Activity消费不同,子View消费掉ACTION_DOWN事件后,ACTION_UP 事件还是会经过父View一层一层传递到子View,最终由子View消费掉。
3.在TouchEventFather的onInterceptTouchEvent返回true,打印的log如下:
从log中可以看出,一旦父View拦截了事件,那么接下来的一序列事件都会交由父View消费。
总结一下事件分发的过程:
点击事件到达顶级View(一般是ViewGroup)以后,一般会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级ViewGroup的拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,如果这时候ViewGroup的OnTouchListener被设置,则OnTouch会被调用,否则OnTouchEvent被调用,如果在OnTouch中返回true则OnTouchEvent就不会被调用。在OnTouchEvent中,如果设置了OnClickListener,则OnClick会被调用。如果顶级ViewGroup不拦截事件,则事件会被传递给点击事件链所在的子View,这时子View的dispatchTouchEvent就会被调用。到此为止,事件已经从顶级View传递到了下一级View,接下来的传递过程和顶级的View是一样的,反复如此,就可完成View事件的分发。
参考《Android开发艺术探索》