onInterceptTouchEvent用于改变事件的传递方向。决定传递方向的是返回值,返回为false时事件会传递给子控件,返回值为true时事件会传递给当前控件的onTouchEvent(),这就是所谓的Intercept(拦截)。
[tisa ps:正确的使用方法是,在此方法内仅判断事件是否需要拦截,然后返回。即便需要拦截也应该直接返回true,然后由onTouchEvent方法进行处理。]
onTouchEvent用于处理事件,返回值决定当前控件是否消费(consume)了这个事件。尤其对于ACTION_DOWN事件,返回true,表示我想要处理后续事件;返回false,表示不关心此事件,并返回由父类进行处理。
可能你要问是否消费了又区别吗,反正我已经针对事件编写了处理代码?答案是有区别!比如ACTION_MOVE或者ACTION_UP发生的前提是一定曾经发生了ACTION_DOWN,如果你没有消费ACTION_DOWN,那么系统会认为ACTION_DOWN没有发生过,所以ACTION_MOVE或者ACTION_UP就不能被捕获。
在没有重写onInterceptTouchEvent()和onTouchEvent()的情况下(他们的返回值都是false), 对上面这个布局,MotionEvent事件的传递顺序如下:
当某个控件的onInterceptTouchEvent()返回值为true时,就会发生截断,事件被传到当前控件的onTouchEvent()。如我们将LayoutView2的onInterceptTouchEvent()返回值为true,则传递流程变成:
如果我们同时将LayoutView2的onInterceptTouchEvent()和onTouchEvent()设置成true,那么LayoutView2将消费被传递的事件,同时后续事件(如跟着ACTION_DOWN的ACTION_MOVE或者ACTION_UP)会直接传给LayoutView2的onTouchEvent(),不传给其他任何控件的任何函数。同时传递给子空间一个ACTION_CANCEL事件。传递流程变成(图中没有画出ACTION_CANCEL事件):
[tisa ps:总体来看, onInterceptTouchEvent是自rootview向下传递, onTouchEvent正好相反。]
正如命名一样,onInterceptTouchEvent用来拦截事件,onTouchEvent用来处理事件,网上大部分的文章中也都对这两类方法的使用情况进行了简单的说明。
事件传递如下图,逐级向下看onInterceptTouchEvent()是否需要截断事件,如果没发生截断,则逐级向上寻找能够处理该事件的onTouchEvent()。
源码上RelativeLayout等布局是继承ViewGroup,看源码上ViewGroup中的onInterceptTouchEvent却非常简单:
publicbooleanonInterceptTouchEvent(MotionEvent ev) {
returnfalse;
}
如果不重写该方法直接就会return false。
如果在其中一层截断事件,让onInterceptTouchEvent()返回true,例如在上图中ChildLayout中发生截断,则事件传递则会向跳过MyView,直接从这一层进入onTouchEvent。若ChildLayout的onTouchEvent返回false,则传递流程如下图
很重要的一点:事件传递是这样的,你可以在传递到响应的方法里面做任何你想做的事情,这些事情不会改变事件传递,能影响事件传递的是这些方法的返回值。例如你在ChildLayout的onTouchEvent做了很多事情,只要返回了false,事件还是会进入到ParentLayout的onTouchEvent。
若保持ChildLayout的onInterceptTouchEvent返回true,将它的onTouchEvent返回true,则事件不会再进入其他组件的onTouchEvent,后续的时间会依次进入这个返回了true的onTouchEvent,传递则变为如下图:
自此说的内容都跟博客开头那个链接里面的文章类似。下面写一些那篇文章里面没有谈及或者不够全面的地方。下文中举例进行的操作都是拖动,即ACITON_DOWN -> ACTION_MOVE -> ACTION_UP
那篇文章里面说在事件发生截断的时候,会像子View发出ACTION_CANCEL,但是我在ChildLayout的onInterceptTouchEvent里面return true之后,却没有收到ACTION_CANCEL,经实验发现,谈谈关于ACTION_CANCEL的事情。ACTION_CANCEL的发出是有条件的:如果子一层曾经处理过事件,即事件进入到onTouchEvent中,则此时截断事件,上一层会向下一层发出ACTION_CANCEL。例如在ChildLayout的onInterceptTouchEvent中对event.getAction()进行判断,如果是ACTION_DOWN,返回false,在ACTION_MOVE的时候,返回true进行截断。则可以发现MyView接到了action为ACTION_DOWN的事件,但是当出现ACTION_MOVE在ChildLayout截断发生时,MyView则收到了ACTION_CANCEL的消息。代码如下:
ChildLayout
@Override
publicbooleanonInterceptTouchEvent(MotionEvent ev) {
booleanresult =super.onInterceptTouchEvent(ev);
switch(ev.getAction()) {
caseMotionEvent.ACTION_DOWN:
Log.i("ZZZZ","ChildLayout onInterceptTouchEvent ACITON_DOWN:");
break;
caseMotionEvent.ACTION_MOVE:
Log.i("ZZZZ","ChildLayout onInterceptTouchEvent ACITON_MOVE:");
result =true;
break;
caseMotionEvent.ACTION_CANCEL:
Log.i("ZZZZ","ChildLayout onInterceptTouchEvent ACITON_CANCEL:");
break;
caseMotionEvent.ACTION_UP:
Log.i("ZZZZ","ChildLayout onInterceptTouchEvent ACITON_UP:");
break;
}
Log.i("ZZZZ","ChildLayout onInterceptTouchEvent return "+result);
returnresult;
}
@Override
publicbooleanonTouchEvent(MotionEvent ev) {
booleanresult =super.onTouchEvent(ev);
switch(ev.getAction()) {
caseMotionEvent.ACTION_DOWN:
Log.i("ZZZZ","ChildLayout onTouchEvent ACTION_DOWN");
break;
caseMotionEvent.ACTION_MOVE:
Log.i("ZZZZ","ChildLayout onTouchEvent ACTION_MOVE");
break;
caseMotionEvent.ACTION_CANCEL:
Log.i("ZZZZ","ChildLayout onTouchEvent ACTION_CANCEL");
caseMotionEvent.ACTION_UP:
Log.i("ZZZZ","ChildLayout onTouchEvent ACTION_UP");
break;
}
result =true;
Log.i("ZZZZ","ChildLayout onTouchEvent return "+result);
returnresult;
}
为了增加事件的复杂性,挑选了有滚动效果的GridView在ChildLayout下一层的MyView作为实验对象
MyGridView
@Override
publicbooleanonInterceptTouchEvent(MotionEvent ev) {
booleanresult =super.onInterceptTouchEvent(ev);
switch(ev.getAction()) {
caseMotionEvent.ACTION_DOWN:
Log.d("ZZZZ","MyGridView onInterceptTouchEvent ACITON_DOWN:");
break;
caseMotionEvent.ACTION_MOVE:
Log.d("ZZZZ","MyGridView onInterceptTouchEvent ACITON_MOVE:");
break;
caseMotionEvent.ACTION_CANCEL:
Log.d("ZZZZ","MyGridView onInterceptTouchEvent ACITON_CANCEL:");
break;
caseMotionEvent.ACTION_UP:
Log.d("ZZZZ","MyGridView onInterceptTouchEvent ACITON_UP:");
break;
}
Log.d("ZZZZ","MyGridView onInterceptTouchEvent return "+result);
returnresult;
}
@Override
publicbooleanonTouchEvent(MotionEvent ev) {
booleanresult =super.onTouchEvent(ev);
switch(ev.getAction()) {
caseMotionEvent.ACTION_DOWN:
Log.d("ZZZZ","MyGridView onTouchEvent ACTION_DOWN");
break;
caseMotionEvent.ACTION_MOVE:
Log.d("ZZZZ","MyGridView onTouchEvent ACTION_MOVE");
break;
caseMotionEvent.ACTION_CANCEL:
Log.d("ZZZZ","MyGridView onTouchEvent ACTION_CANCEL");
break;
caseMotionEvent.ACTION_UP:
Log.d("ZZZZ","MyGridView onTouchEvent ACTION_UP");
break;
}
Log.d("ZZZZ","MyGridView onTouchEvent return "+result);
returnresult;
运行日志:
再次修改代码,取消ChildLayout里onInterceptTouchEvent对事件的拦截返回false,让MyGridView的onTouchEvent方法return super.onTouchEvent(ev)默认返回true。
在同样尝试拖动效果的时候,按照上面的说法,理论上在ParentLayout,ChildLayout,MyGridView都不拦截事件return false的状态下,事件会通过依次通过ParentLayout,ChildLayout,MyGridView的onInterceptTouchEvent,然后到达MyGridView的onTouchEvent,但是我们发现事实并不是如此,移动一段距离后,就没有任何onInterceptTouchEvent执行了。运行效果如下:
说好的依次传递并没有发生,无奈只能去源码中寻找原因,GridView继承于AbsListView,与之前ChildLayout,ParentLayout继承与ViewGroup不同,在AbsListView中的onTouchEvent的ACTION_MOVE的这个case中看到有个startScrollIfNeeded方法,点进去才发现有方法说明“Check if we have moved far enough that it looks more like a scroll than a tap”, 在我们tap屏幕的时候有一段距离touchSlop,小于这个距离的ACTION_MOVE是会被判定成为tap效果的,所以在这段源码里面能看到
if(overscroll || distance > mTouchSlop) {
createScrollingCache();
if(overscroll) {
mTouchMode = TOUCH_MODE_OVERSCROLL;
mMotionCorrection =0;
}else{
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = deltaY >0? mTouchSlop : -mTouchSlop;
}
finalHandler handler = getHandler();
// Handler should not be null unless the AbsListView is not attached to a
// window, which would make it very hard to scroll it... but the monkeys
// say it's possible.
if(handler !=null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
setPressed(false);
View motionView = getChildAt(mMotionPosition - mFirstPosition);
if(motionView !=null) {
motionView.setPressed(false);
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
// Time to start stealing events! Once we've stolen them, don't let anyone
// steal from us
finalViewParent parent = getParent();
if(parent !=null) {
parent.requestDisallowInterceptTouchEvent(true);
}
scrollIfNeeded(y);
returntrue;
}
里面有代码,对parent判空了执行requestDisallowInterceptTouchEvent(true);看到注释则说明的很清楚了,如果满足了条件,被判定成为looks more like a scroll than a tap,则start stealing events, once we've stolen them, don't let anyone steal from us,好傲娇的样子,直接屏蔽了各种有可能阻截这些事件的情况,然后能够阻截这个MotionEvent的就只有onInterceptTouchEvent了,这也就是为什么在有GridView的情况下,"ACTION_DOWN -> ACTION_MOVE -> ACTION_UP"操作一小距离之后却看不到任何onInterceptTouchEvent被执行的原因了
参考:http://blog.csdn.net/guitk/article/details/7057155
http://waynehu16.iteye.com/blog/1926741