http://www.myexception.cn/android/1354963.html
Android中对GridView, ListView等滚动控件的Touch事件onInterceptTouchEvent,onTouchEvent理解
在开始正文之前,首先得感谢http://blog.csdn.net/guitk/article/details/7057155提供的一篇转载文章,里面说的内容和插图让我对TouchEvent的事件传递迅速有了一个大致的印象。但是文章里面有说的不全面不详细的地方,特发此博客记录个人对于这类问题的理解,用来补充和完善,有不妥的地方,也欢迎大家批评讨论。
正如命名一样,onInterceptTouchEvent用来拦截事件,onTouchEvent用来处理事件,网上大部分的文章中也都对这两类方法的使用情况进行了简单的说明。
事件传递如下图,逐级向下看onInterceptTouchEvent()是否需要截断事件,如果没发生截断,则逐级向上寻找能够处理该事件的onTouchEvent()。
源码上RelativeLayout等布局是继承ViewGroup,看源码上ViewGroup中的onInterceptTouchEvent却非常简单:
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return false;
- }
如果不重写该方法直接就会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
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- boolean result = super.onInterceptTouchEvent(ev);
- switch(ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.i("ZZZZ", "ChildLayout onInterceptTouchEvent ACITON_DOWN:");
- break;
- case MotionEvent.ACTION_MOVE:
- Log.i("ZZZZ", "ChildLayout onInterceptTouchEvent ACITON_MOVE:");
- result = true;
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.i("ZZZZ", "ChildLayout onInterceptTouchEvent ACITON_CANCEL:");
- break;
- case MotionEvent.ACTION_UP:
- Log.i("ZZZZ", "ChildLayout onInterceptTouchEvent ACITON_UP:");
- break;
- }
- Log.i("ZZZZ", "ChildLayout onInterceptTouchEvent return "+result);
- return result;
- }
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- boolean result = super.onTouchEvent(ev);
- switch(ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.i("ZZZZ", "ChildLayout onTouchEvent ACTION_DOWN");
- break;
- case MotionEvent.ACTION_MOVE:
- Log.i("ZZZZ", "ChildLayout onTouchEvent ACTION_MOVE");
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.i("ZZZZ", "ChildLayout onTouchEvent ACTION_CANCEL");
- case MotionEvent.ACTION_UP:
- Log.i("ZZZZ", "ChildLayout onTouchEvent ACTION_UP");
- break;
- }
- result = true;
- Log.i("ZZZZ", "ChildLayout onTouchEvent return "+result);
- return result;
- }
为了增加事件的复杂性,挑选了有滚动效果的GridView在ChildLayout下一层的MyView作为实验对象
MyGridView
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- boolean result = super.onInterceptTouchEvent(ev);
- switch(ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.d("ZZZZ", "MyGridView onInterceptTouchEvent ACITON_DOWN:");
- break;
- case MotionEvent.ACTION_MOVE:
- Log.d("ZZZZ", "MyGridView onInterceptTouchEvent ACITON_MOVE:");
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.d("ZZZZ", "MyGridView onInterceptTouchEvent ACITON_CANCEL:");
- break;
- case MotionEvent.ACTION_UP:
- Log.d("ZZZZ", "MyGridView onInterceptTouchEvent ACITON_UP:");
- break;
- }
- Log.d("ZZZZ", "MyGridView onInterceptTouchEvent return "+result);
- return result;
- }
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- boolean result = super.onTouchEvent(ev);
- switch(ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.d("ZZZZ", "MyGridView onTouchEvent ACTION_DOWN");
- break;
- case MotionEvent.ACTION_MOVE:
- Log.d("ZZZZ", "MyGridView onTouchEvent ACTION_MOVE");
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.d("ZZZZ", "MyGridView onTouchEvent ACTION_CANCEL");
- break;
- case MotionEvent.ACTION_UP:
- Log.d("ZZZZ", "MyGridView onTouchEvent ACTION_UP");
- break;
- }
- Log.d("ZZZZ", "MyGridView onTouchEvent return "+result);
- return result;
运行日志:
再次修改代码,取消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;
- }
- final Handler 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
- final ViewParent parent = getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
- scrollIfNeeded(y);
- return true;
- }
里面有代码,对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被执行的原因了。
前几天写代码里面涉及到了GridView和ListView的处理,看到打出来的LOG并不是如之前理解的,故有了这篇博客,用来给自己加深印象,也给有需要的同学提供帮助。
如上文中有不妥的地方欢迎各位批评讨论,谢谢。