想必大家对编写自定义控件的流程不陌生,独自编写过许多继承View、ViewGroup之类的自定义控件。在编写的过程中肯定要考虑到View的事件分发机制,不可避免的重要部分,各位有考虑过以下问题:
1. 事件的传递机制?
2. 事件的分发过程涉及到的方法?
3. 接收事件的方法的优先级?
接下来的内容依次来解析:
(以下融合个人理解和任玉刚老师的《Android开发艺术探索》书中内容)
点击事件的传递规则
1.方法介绍
以下分析的主要对象就是 点击事件(MotionEvent) 。而点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程。当屏幕上的点击事件产生后,Android系统会将此事件传递一个具体的View去响应,而这个传递的过程就是分发过程。
接下来介绍的三种方法就是来完成点击事件的分发过程:
1). dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果点击事件能够传递到当前View,那此方法一定会被调用。
返回结果受当前 View 的 onTouchEvent 和下级View的dispatchTouchEvent方法的影响(若下级View指定消费点击事件,则它的返回结果是false),表示是否消耗当前事件。
2). onIterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用于判断是否拦截点击事件。若当前View拦截了点击事件,在同一个事件序列中,此方法不会再次被调用。返回结果表示是否拦截当前点击事件。
3). onTouchEvent
public boolean onTouchEvent(MotionEvent event)
也是在 dispatchTouchEvent 方法中调用,用于处理具体点击事件(最常用的三个:DOWN、UP、MOVE)。返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前无法再次接收到事件!
【举个栗子:】
比如说在onTouchEvent 方法中分别监视了DOWN、UP、MOVE三种状态,并有对应的逻辑处理,若不将返回结果改为true,则手指在屏幕上操作时,只会响应 DOWN 的逻辑,后面的则忽略掉!
【未修改返回值:】
【修改返回值true:】
以上GIF动图明显对比出不同了,这是一个自定义粘性控件,我在onTouchEvent 方法中分别监视了DOWN、UP、MOVE三种状态,并有对应的逻辑处理。而第一个GIF动图中方法返回值未修改:
return super.onTouchEvent(event);
所以如上所说,由于返回值为修改,它检测到DOWN事件后,做出响应,而后的MOVE、UP事件都无法检测,显示出来的效果是卡顿的感觉。
而第二个GIF动图中设置了返回值:
return true;
表示消耗当前事件,所以在同一个事件序列中,可以再次接收到事件!而这才是我们需要的结果。
由此可见返回值的重要性,接下来继续深入了解这三个方法:
2. 三个方法之间的联系
其实上述三个方法的区别与联系可用以下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过以上伪代码,可得知点击事件的传递规则:当一个点击事件产生后,一个根ViewGroup会最先接收!这时它的 dispatchTouchEvent 方法会被调用:
1). 若此方法返回值为true,代表它要拦截此事件,所以此点击事件会交给此 ViewGroup 处理,即它的onTouchevent 方法随之被调用;
2). 若此方法返回值为false,代表它不拦截此事件,这时当前事件被继续传递给它的子元素,接着子View 的dispatchTouchEvent 方法会被调用,再根据返回值做出选择,如此反复直到此点击事件被最终处理!
3. OnTouchListener 和 onTouchEvent 和 OnClickListener 优先级
//OnTouchListener
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return false;
}
});
//onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
//OnClickListener
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
当一个View 需要处理事件时,如果它设置了 OnTouchListener ,则 OnTouchListener 中的onTouch 方法会被回调。这时事件如何处理取决于onTouch 方法的返回值:
1)若返回false,则当前View的 onTouchEvent 方法会被调用。
2)若返true,此点击事件已被OnTouchListener 中的onTouch 方法消费掉, onTouchEvent 方法不会被调用!
可得出部分结论:给View设置的OnTouchListener ,其优先级比onTouchEvent 高!
而在onTouchEvent 方法中,如果View 还设置了 OnClickListener,这时它的onClick 方法才会被调用!
最终结论:
优先级排序:OnTouchListener >onTouchEvent >OnClickListener
4. 点击事件的传递顺序(Activity 、Window 、View )
当一个点击事件产生后,传递顺序为: Activity —> Window –> View ,即事件总是先传递给Activity ,再传递给Window,最后Window传递给顶级View。而顶级View 接收到此点击事件,就会按照事件分发机制(即上面的逻辑图)。
考虑一个特殊情况:如果一个子View 的 onTouchEvent 返回false,那么它的父容器的 onTouchEvent 会被调用,依次类推。若所有的元素不处理此事件,那么此点击事件最终传递给Activity处理,因此Activity的 onTouchEvent 方法会被调用!
举一个通俗易懂的栗子:这个点击事件就是一个苹果,而Activity就是爷爷,依次传递的是爸爸和儿子。现在爷爷收到一个苹果,心疼上班辛苦的爸爸,给他吃,而爸爸心疼自己的小儿子,给儿子吃。儿子拿到了苹果,嫌弃苹果不新鲜,又还给了爸爸(onTouchEvent返回false),爸爸一看是有些不新鲜,还给了爷爷,爷爷舍不得丢,自己处理掉了(上级的onTouchEvent被调用)。这就是一个传递过程(领导与员工的例子也很贴切)
事件传递机制的一些结论及注意
1. 事件序列
(1). 定义:同一事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。即Touch时最明显的三个状态,事件序列从DOWN 事件开始,手指在屏幕上滑动即MOVE事件,最后以手指抬起 UP事件结束掉这个事件序列。
(2)注意:某个View一旦拦截此点击事件,那么这个事件序列都会由它来处理,并且它的onInterceptTouchEvent不会被调用!也证实了一开始所讲dispatchTouchEvent方法中返回true后,无需再调用此View的onInterceptTouchEvent来确定是否需要拦截。
正常情况下,一个事件序列只能被一个View拦截且消耗。
2. ViewGroup
ViewGroup默认不拦截任何事件。Android源码中ViewGroup的 onInterceptTouchEvent 方法默认返回值为false.
3.View
(1) View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,它的onTouchEvent方法会被调用。
(2) View 的 onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 同时为false)。View的 longClickable 属性默认值为false,clickable属性要分情况:比如Button的 clickable 属性默认为true,而TextView的clickable属性默认为false。
(3) View的enable 属性不影响 onTouchevent 的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者 longClickable属性默认为true,而TextView的clickable属性默认为false.
(4) onClick 会发生的前提是当前View是可点击的,并且它收到了DOWN、UP的事件。
4 .总结
事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中敢于父元素的事件分发过程,ACTION_DOWN事件除外。
实例证明
1. 自定义控件(ViewGroup)
如上图所示,这里是一个彷QQ侧滑栏的自定义控件(ViewGroup)。很明显,操作过程中产生了点击事件:右滑显示侧边栏,点击可直接关闭侧边栏。
这里需要注意的是两个不同的子View(主界面和侧滑栏)里还有下级View(listView),所以当点击事件传递到自定义控件SlideMenu这个View的时候,必须拦截下来,消费掉此点击事件!
所以我们需要重写onInterceptTouchEvent 方法和onTouchEvent 方法,并且修改两个方法的返回值!
//3.触摸事件传递及拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
//返回true,代表消费此事件,自己来处理
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
由以上代码可知,这里重写并修改了返回值,onTouchEvent 方法中返回值为true,代表消耗此点击事件!也许你有个疑问:这里重写了两个方法,那第一个方法dispatchTouchEvent?
正因如此,证实了在介绍dispatchTouchEvent 方法时说的:返回结果受当前 View 的 onTouchEvent 和下级View的dispatchTouchEvent方法的影响!所以重写的两个方法中已经决定此方法的返回值。
2. 自定义控件(View)
如上图所示,这是个粘性自定义控件(View),对屏幕的操作产生了一系列的事件序列,所以此控件需要消费掉此点击事件!
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
case MotionEvent.ACTION_UP:
......
break;
}
invalidate();
return true;
}
以上代码所示,这里的事件序列是从 DOWN事件开始产生的,持续MOVE事件,最后以手指抬起UP事件结束!所以重写onTouchEvent 方法,并且修改返回值为true!
同时也证实了上述结论中的一点:
View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,它的onTouchEvent方法会被调用。
以上大部分都是摘于任玉刚老师的《Android开发艺术探索》,非常好的一本书!详细介绍了View部分,而这边博客也是基于“View的事件分发机制”这一点进行讲解,考虑到纯理论部分有些枯燥,加了一些例子讲解,后面再写一篇源码分析的,也是书中的内容,推荐大家~
希望对你们有帮助:)