事件分发机制:
事件分发机制主要有三个方法,分别是:diapatchTouchEvent() , onInterceptTouchEvent() , onTouchEvent()。
用一段伪代码来表示它们之间的关系:
//对于一个根ViewGroup来说,点击事件会传递给dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
//如果onIntercepteTouchEvent()返回为true,则表示它要拦截当前事件,接着事件将会交由该ViewGroup处理
if(!onInterceptTouchEvent(ev)){
//它的onTouchEvent将会被调用
consume = onTouchEvent(ev);
}else{
//如果onInterceptTouchEvent()方法返回的是false,当前事件将会继续传给他的子元素,一直反复这个过程
consume = dispatchTouchEvent(ev);
}
return consume;
}
一个点击事件的产生:
同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻。在你这个过程将产生一系列的事情,这个事情的序列以down开始,中间含有数不清的move,最终以up结束。 down ---> move.......move -------> up。
当一个view需要处理事件时,如果它设置了onTouchListener,那么onTouchListener中的onTouch方法将会被调用。如果onTouch方法中返回的是false的话,那么当前view的onTouchEvent()方法将会被调用。如果它返回的是true,那么它的onTouchEvent方法将不会被调用。由此可见:onTouchEventListener()和onTouchEvent()的优先级不同,onTouchListener()的优先级 > onTouchEvent()。这里在拓展讲一下点击事件的优先级,onClickListener(),较前两者它的优先级是最低的。即:onTouchListener()> onTouchEvent()> onClickListener()。
点击事件的传递:
点击事件的传递顺序:Activity---->Window-------->顶级View,然后顶级view再对事件进行分发。
下面具体分析一种情况:如果某view的onTouchEvent返回为false,那么它的父容器的onTouchEvent方法将被调用。为这个过程画一个图,便于大家理解:
那么事件是如果传到activity呢?
点击事件由MotionEvent来表示,
结论性话语:
1、事件序列:down ---> move.......move -------> up。
2、一个事件只能被一个view拦截且消耗。
3、某个view一旦决定拦截事件,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent方法是不会再调用的。
4、某个view一旦开始处理事件,如果它不消耗ACTION_DOWN事件(即:它的onTouchEvent方法返回为false),那么同一事件序列当中的其它事件也不会交由它处理。那么事件就会往上传递,交给它的父元素处理,即父元素的onTouchEvent方法将会被调用,以此内推。如果父元素一指没消耗此事件,那么它会一直向上传递,直到传给activity,如果activity还没有消耗掉的话,就会报异常了。
5、如果view消耗了ACTION_DOWN事件,而不消除action_down以外的事件,那么这个点击事件会消失。此时父元素的onTouchEvent方法不会被调用。最终这些消失的事件将会传递给activity处理。
6、view是没有onInterceptTouchEvent方法,它直接交予onTouchEvent()方法处理。
7、viewGroup默认不拦截任何事件。因为在android源码中,viewGroup 的 onInterceptTouchEvent()方法返回的是false。
8、view的默认都是消耗事件的。因为在android源码中,view的onTouchEvent() 返回为true,除非它是不可点击的。对于view的是否可点击是这样的:longClickable()默认是false;clickable()要看具体情况,如Button默认为true,TextView为false。
9、view的enable属性不影响onTouchEvent()的默认返回值。
10、onClick发生的前提是当前view是可点击的,并且它收到了down和up的事件。
11、事件传递总是先传递给父元素,然后再由父元素分发给子元素的。通过requestDisallowInterceptTouchEvent()的方法可以在子元素中干预父元素的分发过程。
事件最先传递给当前activity,由activity的dispatchTouchEvent进行分发。
下面我们看一下这段activty的 dispatchTouchEvent源码:
//当一个view需要处理事件时
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return true;//这里说明,如果如果子view的 diapatchTouchEvent 方法如果返回true的话,整个事件循环就结束来
//如果返回的是false 那么就说明子view没有把这个事件给消费掉,这个事件将会传给activity的diapatchTouchEvent方法
}
return onTouchEvent(ev);
}
我们想知道window是如何将事件传给 viewGroup(根view) 的,通过源码我们知道window是一个抽象类,而window的 superDispatchTouchEvent 方法也是一个抽象方法,因此我们要找到window的实现类才可。
通过源码查找,phoneWindow 就是window的实现类。
那么我们看一下phoneWindow是如何实现点击事件的。在phoneWindow 中,我们可以看到
window如何将事件传递给viewGroup:
//activity的 dispatchTouchEvent 方法
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
//从这里可以看出 activity 的dispatchTouchEvent 方法,将会调用 window 的 superDispatchTouchEvent 方法
if(getWindow().superDispatchTouchEvent(ev)){
return true;//如果子view的 diapatchTouchEvent 方法如果返回true的话,整个事件循环就结束来
//如果返回的是false 那么就说明子view没有把这个事件给消费掉,这个事件将会传给activity的 onTouchEvent 方法
}
return onTouchEvent(ev);
}
//Window是一个抽象类,它的实现类是 phoneWindow ,这里是phoneWindow的 superDispatchTouchEvent 方法
public boolean superDispatchTouchEvent(MotionEvent event){
//由这里可以看出,此时 phoneWindow 又将调用 DecorView 的 superDispatchTouchEvent 方法
return mDecor.superDispatchTouchEvent(event);
}
//那么这个 DecorView 又是什么东西呢 ?
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{
public final View getDecorView(){
if(mDecor == null){
installDecor();
}
return mDecor;
}
}
//看看上面的代码我们知道,事件传递到 DecorView 这里,由于 DecorView 继承自 FrameLayout 且是父 view,所以最终事件会传递给view
/**
* 为什么 DecorView 是父view呢,因为 getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
* 可以获取activity所置的 view , 这个view是 activity的setContentView 所设置的view 的父view 。
*/
那么顶级view又是怎样把事件传递给子view的呢?
如果事件达到顶级view后,如果是viewGroup的情况,首先会调用viewGruop的 dispatchTouchEvent 方法
//下面这是viewGroup的dispatchTouchEvent 的具体方法
final boolean intercepter;
//如果viewGroup不拦截事件,并将事件传递给子元素 则此时 mFirstTouchTarget != null
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept){
//如果决定不拦截事件,则onInterceptTouchEvent将会被调用
intercept = onInterceptTouchEvent(ev);
ev.setAction(action);
}else{
//如果决定拦截事件,则onInterceptTouchEvent 方法不会再被调用
intercept = false;
}
}else{
//onInterceptTouchEvent 方法不会再被调用
intercept = true;
}
在一种特殊情况下:如果在子view中设置 requestDisallowInterceptTouchEvent() 为 FLAG_DISALLOW_INTERCEPT,viewGroup将无法拦截action_down以外的事件。为什么action_down要除外呢,因为面对action_down事件时,ViewGroup总会调用自己的 onIterceptTouchEvent()方法来询问自己是否要拦截。
下面我们看一下为什么在子view中设置FLAG_DISALLOW_INTERCEPT 时, 会拦截 action_down 事件,看下面源码:
//ViewGroup会在action_down 事件道来之前做重置状态操作
if(actionMasked == MotionEvent.ACTION_DOWN){
cancelAndClearTouchTargets(ev);
resetTouchState();//在此方法当中,会对FLAG_DISALLOW_INTERCEPT进行重置
}
当viewGroup不拦截事件的时候,事件会向下分发。
如何下发的,分析源码:
首先会遍历 ViewGroup 的所有子元素,然后判断子元素是否可以接收到点击事件,是否能够接收到子元素的判断依据:
1、子元素是否在播放动画
2、点击事件的坐标是否落在子元素的区域内。
如果某个子view满足这两个条件,那么事件就交由这个子view处理。
下面我们看一下 View 对点击事件的处理过程:
View对点击事件的处理的稍微简单一点,因为view是一个单独的元素,它没有子view,因此无法向下传递,所以它只能自己处理事件。从源码中可以看出view对点击事件的处理,首先它会判断有没有onTouchListener() 方法,如果有 且onTouchListener 方法返回的是true,那么onTouchEvent()就不会被调用了。
下面看一下onTouchEvent()方法是如何消耗事件的?
从源码当中可以看出:只要view的 clickable 和 long_clickable 有一个为true,那么这个事件就会消耗掉,即onTouchEvent() 方法返回为true。
这里说一下 默认view的 long_clickable 为false,而clickable要视具体view而定。在源码中我们可以看出:
setClickable() 和 setLongClickable() 可以分别改变view 的CLICKEABLE 和 LONG_CLICKABLE的属性,将其设置为true。