Android事件分发分为View和ViewGroup的分发,由dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent三个方法共同完成分发过程。
View的事件分发
View的事件分发由dispatchTouchEvent,onTouchEvent两个方法完成。
我们先来看一下一个完整事件的执行流程。
在页面上有一个TestTextView(继承自TextView)。为了方便查看结果,在TestTextView中的dispatchTouchEvent,onTouchEvent方法中打了Log日志
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("tag","===== onTouchEvent "+ event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.e("tag","===== onTouchEvent " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.e("tag","===== onTouchEvent " + event.getAction());
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("tag","===== dispatchTouchEvent " + event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.e("tag","===== dispatchTouchEvent " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.e("tag","===== dispatchTouchEvent " + event.getAction());
break;
}
return super.dispatchTouchEvent(event);
}
设置TestTextView的点击事件和touch监听
tv_touch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("tag","======== onClick");
}
});
tv_touch.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("tag","===== onTouch "+ event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.e("tag","===== onTouch " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.e("tag","===== onTouch " + event.getAction());
break;
}
return false;
}
});
点击TextView查看Log日志:
11-24 16:20:46.870 10998-10998/com.zy.touchevent E/tag: ===== dispatchTouchEvent 0
11-24 16:20:46.871 10998-10998/com.zy.touchevent E/tag: ===== onTouch 0
11-24 16:20:46.871 10998-10998/com.zy.touchevent E/tag: ===== onTouchEvent 0
11-24 16:20:46.897 10998-10998/com.zy.touchevent E/tag: ===== dispatchTouchEvent 1
11-24 16:20:46.897 10998-10998/com.zy.touchevent E/tag: ===== onTouch 1
11-24 16:20:46.897 10998-10998/com.zy.touchevent E/tag: ===== onTouchEvent 1
11-24 16:20:46.898 10998-10998/com.zy.touchevent E/tag: ===== onClick
从Log日志上可以看出,点击页面上的一个View,事件传递的方法顺序是 dispatchTouchEvent ->onTouch ->onTouchEvent->onClick ;
先看dispatchTouchEvent 方法
/**
* 分发事件,因为是view的事件分发此时分发的对象是自己
* 返回值是boolean类型,表示是否消费当前事件
* @param event
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
看一下dispatchTouchEvent的源码:
boolean result = false;//返回结果,默认false不处理
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//前面如果没有处理,result为false,此时result结果受onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
从上面代码可以看出,result默认为false不处理,且result的结果在两个if中确定。
第一个if判断 li != null
在if上面已经初始化,li!=null
成立;
mOnTouchListener 在我们设置setOnTouchListener
监听的时候已经赋值,也不为null;
(mViewFlags & ENABLED_MASK) == ENABLED
判断当前点击的控件是否是enable的,按钮默认都是enable的,成立;
最后一个条件li.mOnTouchListener.onTouch(this, event)
的返回值受OnTouchListener事件中的onTouch()方法影响,如果在onTouch()中返回ture则result=true
成立,就不会执行第二个if语句。
第二个if判断当第一个if判断不成立时,此时result的结果受自身onTouchEvent影响;
既然dispatchTouchEvent的返回值受onTouch()方法影响,onTouch()的默认返回值为false,我们修改为true看看此时的打印日志:
11-24 17:32:13.263 18052-18052/com.zy.touchevent E/tag: ===== dispatchTouchEvent 0
11-24 17:32:13.264 18052-18052/com.zy.touchevent E/tag: ===== onTouch 0
11-24 17:32:13.316 18052-18052/com.zy.touchevent E/tag: ===== dispatchTouchEvent 1
11-24 17:32:13.316 18052-18052/com.zy.touchevent E/tag: ===== onTouch 1
从Log日志我们可以发现onTouchEvent,onclick方法没了,这很好理解。因为onTouch 方法返回true表示将此次事件消费掉,而onTouchEvent方法在onTouch 后面执行,事件自然也就执行不到onTouchEvent和onclick。
既然onTouch 方法能够影响onTouchEvent的执行,那么onTouchEvent又和onclick有没有一腿呢?
来看看onTouchEvent方法的源码(源码有点多,省略和onclick不相关部分)
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
performClick();
}
break;
}
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
当手指抬起时,performClick();
方法会被调用,点击查看方法,可以知道当li.mOnClickListener != null
不为空时,li.mOnClickListener.onClick(this);
view的点击方法就会被调用。
View的事件分发就说到这里了,下面来说说ViewGroup的事件分发:
ViewGroup的事件分发
我觉得这张图是最能简单说明ViewGroup的事件分发顺序了,所以我就借鉴一下 ☺
当一个事件(点击或者touch)到达activity的根节点也就是ViewGroup时,ViewGroup会把事件遍历分发给它包含的每个子View,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。所以上图的分发顺序是① -- ② -- ⑤ -- ⑥ -- ⑦ -- ③ -- ④。如果在分发过程中任何一个子View的dispatchTouchEvent的返回值为true(消费事件),后面的分发都会中止。
看一下ViewGroup的源码吧,很长,会省略很多,我开始看不懂也是参考别人的讲解看的,文章末尾会给链接的。
1、dispatchTouchEvent方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (disallowIntercept || !onInterceptTouchEvent(ev)) { ①
...
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if (child.dispatchTouchEvent(ev)) { ②
mMotionTarget = child;
return true;
}
}
}
return target.dispatchTouchEvent(ev);
}
这里只给了几处影响dispatchTouchEvent方法返回值的代码,第一个判断里面两个条件满足其中一个就会进入后面代码,disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。第二个判断是对onInterceptTouchEvent(拦截事件)返回值取反。如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true,第二个判断条件child.dispatchTouchEvent(ev)为true成立。
ViewGroup也是View的子类,因此View的事件分发在ViewGroup中同样适用。当ViewGroup中没有子View时,走的就是View的分发过程了
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
2、onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
上面就是onInterceptTouchEvent方法的源码,默认返回false不拦截,我们可以在需要的时候进行拦截。
3、onTouchEvent方法
刚刚也已经说了,ViewGroup也是View的一个子类。因此当ViewGroup没有子View时onTouchEvent方法会被调用,还有就是子View的onTouchEvent方法返回值为false,父容器的onTouchEvent方法会被调用。
我在开发艺术探索里看见了一个很好理解的比喻:假如点击事件是一个难题,这个难题被上级领导分配给一个程序员处理(类似事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),但是难题又必须要解决,那就只能交给水平更高的上级解决了(上级的onTouchEvent方法被调用),如果上级在搞不定,那只能交给上级的上级去解决,就这样将难题一层层上抛。
最后关于事件分发的总结:
1、Touch事件分发中只有两个主角:ViewGroup和View。包含onInterceptTouchEvent、
dispatchTouchEvent、onTouchEvent三个相关事件,其中ViewGroup又继承于View。
2、一个点击事件产生后,他的传递过程遵循如下顺序: Activity -> Window -> View
3、触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只
有一个,Move有若干个,可以为0个。
总结:第一次写这种总结分析类的文章,还是很困难的,首先我没有一个贯穿全文的思路(想了几个,被写死了),然后就是不知道该怎么写(虽然我心里明白是啥回事,但说不出来啊/(ㄒoㄒ)/),所以这篇文章里面我借鉴了好多别人的东西,包括思路,代码片段(希望不会被瞧不起)。哈哈,虽然写的不好,但还是有收获的,继续加油~
参考:
《安卓开发艺术探索》
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
Android:30分钟弄明白Touch事件分发机制