点击事件的传递规则
同个事件:
是指手指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束,在整个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的move事件,最终以 up 事件结束。
事件传递规则:
当一个点击事件发生之后,传递过程遵循如下顺序:Activity -> Window -> View
对于跟ViewGroup来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 方法就会被调用,如果这时ViewGroup的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给ViewGroup处理,即它的 onTouchEvent 方法就会被调用;如果这个ViewGroup的 onInterceptTouchEvent 方法返回 false, 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的ispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。如果一个view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。
伪代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件分发过程的三个重要方法:
1 . public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的onTouchEvent和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
2 . public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
ViewGroup中onInterceptTouchEvent方法默认返回false,即默认不拦截任何事件
View中没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
3 . public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接收到事件。
View中的onTouchEvent默认返回true,消耗事件,除非它是不可点击的(clickable和longClickable都为false)。View的longClickable默认是false的,clickable则不一定,Button默认是true,而TextView默认是false。
ViewGroup不设置监听事件的话,onTouchEvent默认返回false,ViewGroup设置监听事件的话,onTouchEvent返回true,消耗事件。
View的enable属性不影响onTouchEvent的默认返回值。哪怕一个view是disable状态的,只要它的clickable或者longClickable有一个是true,那么它的onTouchEvent就会返回true。
某个view一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父容器去处理,即调用父容器的onTouchEvent方法;如果它消耗ACTION_DOWN事件,但是不消耗其他类型事件,那么这个点击事件会消失,此时父元素的onTouchEvent方法并不会被调用,当前view依然可以收到后续的事件(即当前view的onTouchEvent方法会被调用),但是这些事件最后都会传递给Activity处理。(因为正常情况下,一个事件序列只能被一个view拦截并消耗,因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了。)
其它:
1 . 如果给一个view设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前view的onTouchEvent方法会被调用;如果返回true,则onTouchEvent方法将不会被调用。在onTouchEvent方法中,如果当前view设置了OnClickListener,那么它的onClick方法会被调用,所以OnTouchListener的优先级比onTouchEvent高,OnClickListener的优先级最低。
2 . 事件传递过程总是先传递给父元素,然后再由父元素分发给子view,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外,即对于ACTION_DOWN时,ViewGroup会调用自己的onInterceptTouchEvent方法来询问是否要拦截事件。
VIew的滑动冲突
如何根据坐标得到滑动的方向:
根据滑动距离和水平方向形成的夹角;
根据水平和竖直方向滑动的距离差;
根据水平和竖直方向滑动的的速度差等
滑动冲突的解决方式:
1 . 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。
父容器的onInterceptTouchEvent方法伪代码:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要当前点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
事件分发过程:
父容器的onInterceptTouchEvent方法,对于down事件和move父容器不需要当前点击得事件,返回false,才能传到子元素,因为如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了
如果父容器需要某类点击事件,返回true,拦截事件
对于up,返回false,因为在父容器不需要任何类点击事件时,如果在up事件返回true,会导致子元素无法接收到up事件;在父容器需要某类点击事件时,在up事件返回false,不会影响onTouchEvent方法接受到up事件(因为之前move事件,在父容器需要某类点击事件返回true,如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了)
2 . 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。
子元素的dispatchTouchEvent方法伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {]
Parent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
Parent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父容器的onInterceptTouchEvent方法伪代码
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
事件分发过程:
父容器的onInterceptTouchEvent方法,对于down,返回false,才能传到子元素,因为如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了
子元素的dispatchTouchEvent方法,对于dowm,调用Parent().requestDisallowInterceptTouchEvent(true),设置了FLAG_DISALLOW_INTERCEPT,父容器将无法拦截除了down以外的事件(即down以外的事件,父容器不会再调用onInterceptTouchEvent方法),所以down以后的其他事件传到子元素。当父容器需要某类点击事件时,调用Parent().requestDisallowInterceptTouchEvent(false),对FLAG_DISALLOW_INTERCEPT重置,父容器就可以继续调用它自己的onInterceptTouchEvent方法,所以在down以外的事件,父容器的onInterceptTouchEvent方法返回true,拦截事件。
其它:ViewGroup在事件分发时,如果是down事件就会重置FLAG_DISALLOW_INTERCEPT这个标志位,导致之前子元素设置的标志位无效,所以,如果我们想提前处理所以的点,要选择dispatchTouchEvent方法,前提事件能传递到当前的ViewGroup.
参考:Android开发艺术探索