主要介绍内容:
- View的事件分发机制
- 点击事件的传递规则
- 事件分发的源码解析
- View的滑动冲突
- 常见的滑动冲突场景
- 滑动冲突的处理规则
- 滑动冲突的解决方式
在上一章中我们已经介绍了 View 的基础知识以及 View 的滑动,想了解的请戳 Android——View的事件体系(一)View的滑动
这里本节将介绍 View 的一个核心知识点:事件分发机制。在真正进入详细探讨之前,大家可以先去瞅下这篇博文 Android事件分发完全解析之为什么是她 来找到自己要学习 View 事件的出发点。
下面我们开始进入今天的正题
View的事件分发机制
点击事件的传递规则
在介绍点击事件的传递规则之前,首先我们要明白这里要分析的对象就是 MotionEvent,关于 MotionEvent 我们在上一章中已经进行了较为详细的介绍,有兴趣的可以回头去瞅下。所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要吧这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成; dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent, 下面我们先来介绍下这几个方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会调用,返回结果受当前 View 的 onTouchEvent 和 下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在上诉方法内部中调用,用来判断师傅拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
上述的三个方法其实我们可以使用伪代码的形式来描述下它们之间的关系,伪代码如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
上述提供的伪代码已经将 三者之间的关系表现的淋漓尽致。通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这是它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 进行处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最总处理。
当一个View需要处理事件时,如果它设置了 onTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给 View 设置的 OnTouchListener ,其优先级比 onTouchEvent 要高,在 onTouchEvent 方法中,如果当前设置的有 OnClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener,其优先级最低,即处于事件传递的尾端。
想要验证上面结论的真实性的可以参考这两篇博文:
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 在传递给顶级 View。顶级 View 在接收到事件后,就会按照事件分发机制去分发事件。考虑这么一种情况,如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 进行处理,即 Activity 的 onTouchEvent 方法会被调用。这个过程其实也很好理解,我们可以换一种思路,假如点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那只能交给水平更高的上级解决(上级的 onTouchEvent 被调用),如果上级在搞不定,那就只能交给上级的上级去解决,就这样将难题一层层向上抛,这是公司内部一种很常见的处理问题的过程。从这个角度来看,View 的事件传递过程还是很贴近现实的,毕竟程序员也生活在现实中。
下面我们来通过一个例子来描述一下 View 事件拦截在生活中的体现:假设你所在的公司,有一个总经理,级别最高;他下面有一个部长,级别次之;最底层,就是干活的你,没有级别。现在董事会交给总经理一项任务,总经理将这项任务布置给了部长,部长又把任务安排给了你。而当你好不容易干完活了,你就把任务交给部长,部长觉得任务完成得不错,于是就签上他的名字交给总经理,总经理看了也觉得不错,就也签了名字交给董事会。这样,一个任务就顺利完成了。如果大家能非常清楚地理解这样一个场景,那么对应事件拦截机制,你就已经基本入门了。下面我们通过代码来模拟一下该场景:
一个总经理 —— MyViewGroupA,最外层的 ViewGroup
一个部长 —— MyViewGroupB,中间的 ViewGroup
一个干活的你 —— MyView,在最底层
对于 MyViewGroupA 和 MyViewGroupB来说,我们通过继承 LinearLayout 来实现,重写如下所示的三个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA dispatchTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA dispatchTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA dispatchTouchEvent————up");
break;
}
boolean flag = super.dispatchTouchEvent(ev);
// Log.e("mk", "ViewGroupA dispatchTouchEvent======" + flag);
return flag;
// return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————up");
break;
}
boolean flag = super.onInterceptTouchEvent(ev);
// Log.e("mk", "ViewGroupA onInterceptTouchEvent======" + flag);
return flag;
// return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA onTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA onTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA onTouchEvent————up");
break;
}
boolean flag = super.onTouchEvent(event);
// Log.e("mk", "ViewGroupA onTouchEvent======" + flag);
return flag;
// return false;
}
而对于 MyView 来说,我们通过继承 View来实现,重写如下所示的两个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "MyView dispatchTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "MyView dispatchTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "MyView dispatchTouchEvent————up");
break;
}
boolean flag = super.dispatchTouchEvent(ev);
// Log.e("mk", "MyView dispatchTouchEvent======" + flag);
return flag;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "MyView onTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "MyView onTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "MyView onTouchEvent————up");
break;
}
// boolean flag = super.onTouchEvent(event);
// Log.e("mk", "MyView onTouchEvent======" + false);
return false;
}
从上面的代码中可以看到, ViewGroup 级别比较高,比 View 多了一个方法——onInterceptTouchEvent()。这个方法我们前面已经介绍过是用来做拦截事件用的。我们来看一下输出的log:
09-28 00:28:22.360 12206-12206/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 00:28:22.360 12206-12206/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB dispatchTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB onInterceptTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: MyView dispatchTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: MyView onTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB onTouchEvent————down
09-28 00: