您的位置:首页 > 移动开发 > Android开发

Android 学习笔记:View的事件分发机制 解析及实例讲解

2016-10-21 14:09 411 查看
想必大家对编写自定义控件的流程不陌生,独自编写过许多继承ViewViewGroup之类的自定义控件。在编写的过程中肯定要考虑到View的事件分发机制,不可避免的重要部分,各位有考虑过以下问题:

1. 事件的传递机制?

2. 事件的分发过程涉及到的方法?

3. 接收事件的方法的优先级?

接下来的内容依次来解析:

(以下融合个人理解和任玉刚老师的《Android开发艺术探索》书中内容)

点击事件的传递规则

1.方法介绍

以下分析的主要对象就是 点击事件(MotionEvent) 。而点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程。当屏幕上的点击事件产生后,Android系统会将此事件传递一个具体的View去响应,而这个传递的过程就是分发过程

接下来介绍的三种方法就是来完成点击事件的分发过程:

1). dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)


用来进行事件的分发。如果点击事件能够传递到当前View,那此方法一定会被调用。

返回结果受当前 ViewonTouchEvent 和下级ViewdispatchTouchEvent方法的影响(若下级View指定消费点击事件,则它的返回结果是false),表示是否消耗当前事件。

2). onIterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent event)


在上述方法内部调用,用于判断是否拦截点击事件。若当前View拦截了点击事件,在同一个事件序列中,此方法不会再次被调用。返回结果表示是否拦截当前点击事件。

3). onTouchEvent

public boolean onTouchEvent(MotionEvent event)


也是在 dispatchTouchEvent 方法中调用,用于处理具体点击事件(最常用的三个:DOWNUPMOVE)。返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前无法再次接收到事件!

【举个栗子:】

比如说在onTouchEvent 方法中分别监视了DOWNUPMOVE三种状态,并有对应的逻辑处理,若不将返回结果改为true,则手指在屏幕上操作时,只会响应 DOWN 的逻辑,后面的则忽略掉!

【未修改返回值:】



【修改返回值true:】



以上GIF动图明显对比出不同了,这是一个自定义粘性控件,我在onTouchEvent 方法中分别监视了DOWNUPMOVE三种状态,并有对应的逻辑处理。而第一个GIF动图中方法返回值未修改:

return super.onTouchEvent(event);


所以如上所说,由于返回值为修改,它检测到DOWN事件后,做出响应,而后的MOVEUP事件都无法检测,显示出来的效果是卡顿的感觉。

而第二个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,代表它不拦截此事件,这时当前事件被继续传递给它的子元素,接着子ViewdispatchTouchEvent 方法会被调用,再根据返回值做出选择,如此反复直到此点击事件被最终处理!



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,则当前ViewonTouchEvent 方法会被调用。

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 接收到此点击事件,就会按照事件分发机制(即上面的逻辑图)。

考虑一个特殊情况:如果一个子ViewonTouchEvent 返回false,那么它的父容器的 onTouchEvent 会被调用,依次类推。若所有的元素不处理此事件,那么此点击事件最终传递给Activity处理,因此ActivityonTouchEvent 方法会被调用!

举一个通俗易懂的栗子:这个点击事件就是一个苹果,而Activity就是爷爷,依次传递的是爸爸和儿子。现在爷爷收到一个苹果,心疼上班辛苦的爸爸,给他吃,而爸爸心疼自己的小儿子,给儿子吃。儿子拿到了苹果,嫌弃苹果不新鲜,又还给了爸爸(onTouchEvent返回false),爸爸一看是有些不新鲜,还给了爷爷,爷爷舍不得丢,自己处理掉了(上级的onTouchEvent被调用)。这就是一个传递过程(领导与员工的例子也很贴切)

事件传递机制的一些结论及注意

1. 事件序列

(1). 定义:同一事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。即Touch时最明显的三个状态,事件序列从DOWN 事件开始,手指在屏幕上滑动即MOVE事件,最后以手指抬起 UP事件结束掉这个事件序列。

(2)注意:某个View一旦拦截此点击事件,那么这个事件序列都会由它来处理,并且它的onInterceptTouchEvent不会被调用!也证实了一开始所讲dispatchTouchEvent方法中返回true后,无需再调用此ViewonInterceptTouchEvent来确定是否需要拦截。

(3)注意: 某个View一旦开始处理事件,若它消耗ACTION_DOWN 事件(即onTouchEvent返回了 false),那么同一事件序列的其它事件都不会交给它来处理,并交由父元素处理!

正常情况下,一个事件序列只能被一个View拦截且消耗。

2. ViewGroup

ViewGroup默认不拦截任何事件。Android源码中ViewGrouponInterceptTouchEvent 方法默认返回值为false.

3.View

(1) View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,它的onTouchEvent方法会被调用。

(2) ViewonTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickablelongClickable 同时为false)。ViewlongClickable 属性默认值为falseclickable属性要分情况:比如Buttonclickable 属性默认为true,而TextViewclickable属性默认为false

(3) Viewenable 属性不影响 onTouchevent 的默认返回值。哪怕一个Viewdisable状态的,只要它的clickable或者 longClickable属性默认为true,而TextViewclickable属性默认为false.

(4) onClick 会发生的前提是当前View是可点击的,并且它收到了DOWNUP的事件。

4 .总结

事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中敢于父元素的事件分发过程,ACTION_DOWN事件除外。

实例证明

1. 自定义控件(ViewGroup)



如上图所示,这里是一个彷QQ侧滑栏的自定义控件(ViewGroup)。很明显,操作过程中产生了点击事件:右滑显示侧边栏,点击可直接关闭侧边栏。

这里需要注意的是两个不同的子View(主界面和侧滑栏)里还有下级ViewlistView),所以当点击事件传递到自定义控件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 方法时说的:返回结果受当前 ViewonTouchEvent 和下级ViewdispatchTouchEvent方法的影响!所以重写的两个方法中已经决定此方法的返回值。

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的事件分发机制”这一点进行讲解,考虑到纯理论部分有些枯燥,加了一些例子讲解,后面再写一篇源码分析的,也是书中的内容,推荐大家~

希望对你们有帮助:)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: