View事件分发机制
点击事件从activity -> window -> DactorView传递;
MotionEvent:点击事件,包含点击状态:down/move/up/cancel;点击坐标,getX()/getY()/getRowX()/getRowY()。
1. 相关方法:
- disptchTouchEvent(): 分发点击事件
- onInterceptTouchEvent(): 拦截点击事件,ViewGroup才有。
- onTouchEvent():响应点击事件。
伪代码:
public boolean disptchTouchEvent(MotionEvent e) {
boolean consume;
if(onInterceptTouchEvent(e)) {
consume = onTouchEvent(e);
} else {
consume = child.disptchTouchEvent(e);
}
return consume;
}
2. 原则总结:
-
事件序列是指一组1个down事件,n个move事件和1个up事件组成的。
-
正常情况下,一组事件序列都只能被一个View拦截并消耗,除非通过特殊手段强制转移给其他view处理;
-
当ViewGroup决定拦截一组事件序列后(这里指的是down事件),后面的点击事件(down、move和up事件)会有它自己处理,而且都不会调用onInterceptTouchEvent()方法,因为既然决定都要它处理了,以后都不需要再次判断是否需要拦截,符合生活规律。
-
如果down事件被消费掉(onTouchEvent返回true), 以后的move事件和up事件都会发给这个view,如果不消费move和up事件(onTouchEvent返回false),也会不断接收到move事件或up事件,这些事件会消失,不会被发送给parent的onTouchEvent, 最终这些消失的事件会被activity处理。
-
如果down事件没有被消费掉(onTouchEvent返回false),这个事件就会被parent的onTouchEvent处理;以后的move和up事件都不会被再发给这个view了。实际当中,上级让你完成一个任务,你没有完成,把任务交给上司,那上司在短时间内不会再把任何交给你了,对你不信任了。
-
ViewGroud的onInterceptToucheEvent默认是false,不拦截;
-
View没有onInterceptToucheEvent,分到事件后直接调用onTouchEvent。
-
onTouchEvent默认是true,只要有clickable或者longClickable有一个为true。
默认longClickable为false,但是clickable根据不同的控件类型默认值不一样。比如Buttton的clickable默认为true,但是Textview的clickable默认是false。
-
setEnable(false)不会影响事件传递。
-
onClick能执行的前提是是可点击的(clickable和longClickable至少一个为true),并且收到down和up事件。
-
setOnTouchListener以后,这个监听器的onTouch方法的返回值将影响后续的onTouchEvent流程。如果onTouch返回true,后续OnTouchEvent不会执行,onClick也不会执行了。如果onTouch返回false,onTouchEvent接着执行。onTouchEvent返回true并且enable为true,onClick也会在up事件后被调用,。
-
事件传递都是由外到内的,由父元素传到子元素的。通过requestDisallowInterceptTouchEvent能让子元素影响父元素的事件分发过程,但是ACTION_DOWN是无法影响的。
3. 源码分析
Activity::dispatchTouchEvent
//如果在所有的view里都没有消费点击事件,由activity自己兜底消费掉。
public boolean disptchTouchEvent(MotionEvent ev) {
if(ev.getAction == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
ViewGroup::disptchTouchEvent
//mFirstTouchTarget表示子view里有找到消费者。
//如果down事件的时候直接onInterceptTouchEvent返回true,那mFirstTouchTarget == null,那接下来的move和up事件直接走4分支,不会再经过1分支,证明了原则总结第3条。
//如果如果down事件的时候直接onInterceptTouchEvent返回false,然后在子view里找到了消费者,那mFirstTouchTarget != null,以后的move和up事件都可以走1分支。
//mGroupFlags默认为0,所以disallowIntercept为false,默认会进入分支2。
//requestDisallowInterceptTouchEvent(true):disallowIntercept变成true,会进入3分支,ViewGroup直接消费掉;
//requestDisallowInterceptTouchEvent(false):disallowIntercept变成false,会进入分支2;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept) {
//2
intercept = onInterceptTouchEvent(ev);
ev.setAction(ev);
}else{
//3
intercept = false;
}
}else{
//4
intercept = true;
}
4. 滑动冲突
-
外部拦截发(推荐)
在父ViewGroup的onInterceptTouchEvent里做拦截
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; switch(ev.getAction()) { case MotionEvent.ACTION_DOWN: intercept = false; break; case MotionEvent.ACTION_MOVE: if(父类需要的拦截条件) { //这时候move事件就会被自己消费掉。 intercept = true; }else{ //move事件又被下发到子view里消费掉。 intercept = false; } break; case MotionEvent.ACTION_UP: //下发到子view,否则子view的onClick事件无法执行。 intercept = false; break; } return intercept; }
-
内部拦截法
父ViewGroup::onInterfceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_DOWN) { //down事件不拦截,可以让后续其他时间下发到子view return false; }else{ //move和up事件被拦截,自己处理掉 return true; } }
子view::dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) { boolean intercept = false; switch(ev.getAction()) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if(父类需要的拦截条件) { //这时候move事件到不了治理,会调用父类的onInterceptTouchEvent(),返回true,被父类消费。 parent.requestDisallowInterceptTouchEvent(false); }else{ //进入ViewGroup::disptchTouchEvent的分支3,直接发给子view消费 } break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); }
5. 应用层面分析
上图只是示意,并不完全准确,比如onTouchEvent在down事件返回false才会上发整个事件序列给parent的onTouchEvent方法,但如果是down事件返回true,move或者up事件才返回false,则move或up事件不会上发的,事件会消失,消失的事件最后才会发给activity。
重写touch事件返回值角度分析:
-
dispatchTouchEvent():
-
true: 事件序列结束;
-
false: Activity: 事件结束;
非Activity:上发给parent的onTouchEvent()。
-
super: Activity: ViewGroup.dispatchTouchEvent();
ViewGroup: onInterceptTouchEvent();
View: onTouchEvent();
-
-
onInterceptTouchEvent():
-
true: onTouchEvent(),自己消耗事件。
如果down事件被消耗,以后的move和up事件都会直接发给自己的ouTouchEvent()消耗; 如果down事件是子view消耗,只是move或up事件消耗,那会给子view发送一个cancel事件,然后剩下的move或者up事件被自己劫持,调用自己的onTouchEvent()方法;
-
false/super: 子View的dispatchTouchEvent();
-
-
onTouchEvent():
-
true: Activity: 事件结束;
View/ViewGroup: 消耗事件(如果down事件被消耗,以后的move和up事件直接发给自己消耗;如果只是down被消耗,但是move或up事件不消耗(返回false),事件会消失,然后最终发给activity。 -
false: Activity: 事件结束;
View/ViewGroup: 不消耗事件(如果down事件不消耗,以后的事件直接上发给parent的onTouchEvent,再也不会下发给自己;如果是down被消耗,但是move或up事件不消耗(返回false),事件会消失,然后最终发给activity。)
-
super:如果clickable或者longClickable有一个为true,返回就是true,表示消耗事件,默认的longClickable都是false,但是Button的clickable默认为true,TextView的clickable默认为false。
-
view的enable不影响touch事件的分发,但是会影响onClick的执行。
onClick能执行的前提:
- setOnTouchListener的onTouch方法返回true或者没有设置TouchListener。
- view的enable为true;
- onTouchEvent的返回值为true;表示消耗事件,DOWN,MOVE,UP事件都能发过来,尤其UP事件。
优先级: onTouch -> onTouchEvent -> onClick。