[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent

PART 1 dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent

三行概括:
onInterceptTouchEventViewGroup的方法(因为View已经是叶子结点,不能再选择是否intercept了,只能选择是否consume),onTouchEventView的方法。它们都能拦截Touch事件。
onInterceptTouchEventreturn true代表消费事件,不向下传递给子View
onTouchEventreturn true代表传递完毕,向上传递到ViewGroup/ActivityonTouchEvent


1.1

为了解决ViewDragLayout与ScrollView的滚动冲突,或者说想要制作一个无法滚动的ScrollView,去了解了一下这三个回调函数的执行顺序。

dispatchTouchEvent
onTouchEvent
onInterceptTouchEvent


1.2

假如有这样一个页面,包含一个Activity,一个MyLayout (extends framelayout), 一个MyView (extends TextView)

[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第1张图片
1.png
  • Activity里覆写:

    • dispatchTouchEvent()
    • onTouchEvent()
  • MyLayout里覆写:

    • dispatchTouchEvent()
    • onInterceptTouchEvent()
    • onTouchEvent()
  • MyView里覆写:

    • dispatchTouchEvent()
    • onTouchEvent()

并且每个方法都打印这样格式的内容:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TAG", "Activity dispatchTouchEvent DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("TAG", "Activity dispatchTouchEvent DOWN");
                break;
            //other cases..
        }
        boolean b = super.dispatchTouchEvent(ev);
        Log.d("TAG", "Activity dispatchTouchEvent RETURNS " + b);
        return b;
    }

那么 点击一下MyView,会打印出:

  1. Activity dispatchTouchEvent DOWN
    先执行了Activity的dispatchTouchEvent。
    然后从switch中break,执行super.dispatchTouchEvent(ev);,控制权被分发到了MyLayout
  2. MyLayout dispatchTouchEvent DOWN
    然后再次执行MyLayout覆写的dispatchTouchEvent中的super.dispatchTouchEvent(ev);
    分发到MyLayout的onInterceptTouchEvent。
  3. MyLayout onInterceptTouchEvent DOWN
  4. MyLayout onInterceptTouchEvent RETURNS false
    这时候ViewGroup知道它对Intercept Down Event不感兴趣。所以它会把事件分发给所有的children,这里只有一个child就是MyView。
  5. 6..7..见下图:
[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第2张图片
image.png

是一个类似递归(栈)的操作,先进去别的函数再回来再继续执行原来的函数。

1.3

我遇到的问题是自己定义的ViewDragLayout(里面组合了ViewDragHelper)在它的子View的交界处无法通过ViewDragHelper回调的tryCaptureView捕获子View(ScrollView),导致ScrollView获取了焦点,也就是说在触摸边界的时候,ViewDragLayoutonInterceptTouchEvent本应返回true的,实际却返回了false,导致ViewDragLayout对滑动不感兴趣,ScrollView就处理了这次滑动。验证了一下确实是这样:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean b = mViewDragHelper.shouldInterceptTouchEvent(ev);
        System.out.println("---->" + b);
        return b;
    }
[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第3张图片
滑动边界处
[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第4张图片
滑动非边界处

注意只要看最后一个打印的是true或者false就行了,上面这些打印的内容是一瞬间打印出的,持续的滑动并不会持续触发onInterceptTouchEvent。

下面有几个方案:

  1. onInterceptTouchEvent里返回:return MotionEvent.ACTION_MOVE == ev.getAction();
    会先打印false再打印true,false是DOWN操作,开始MOVE之后就true了。这样看起来没问题,我想实现的就是让ScrollView失去焦点嘛,但有两个问题:
  • 我发现即便是轻轻点一下,ACTION_MOVE也会先false再true,说明MOVE操作包含了DOWN和滑动,这样会导致点击ScrollView里的内容都没有响应了。
  • ScrollView里面还有Banner,这么做会让Banner无法滑动。
  1. onInterceptTouchEvent里返回:return MotionEvent.ACTION_DOWN == ev.getAction();
    一直打印true。这样就更不行了。

  2. if (mScroll.getScrollY() > 0) return true;
    本意是当ScrollView的一旦开始滚动就让他失去焦点,然而发现只有在撒手的时候getScrollY才会刷新,不行。

  3. 计算touch的位置相对于边界处的高度,如果高度差<一定的像素就return true。比较困难,因为ScrollView的边界一直在变。

  4. 继承ScrollView,覆写onInterceptTouchEvent(或者onTouchEvent)让它始终返回false (可以想象,ScrollView本身的onInterceptTouchEvent一定是在滑动起来之后就让onInterceptTouchEvent返回true了)。
    这方法很Easy也能达到效果,这样一来ScrollView就丧失了滑动的效果,因为onInterceptTouchEvent返回false代表不消费这次操作,把操作递交给子View去处理,而子View是LinearLayout不能滑动,所以就没法处理,而据我猜测LinearyLayout和RelativeLayout这种基本的Layout一定是直接返回false的,所以他们里面的ViewPager之类的东西是始终可以获取到Touch事件的。采用。

PART 2 事件分发机制

之前看书的时候,作者举了个伪代码的例子,还是蛮形象的:


[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第5张图片
图2.1 ViewGroup的事件分发伪代码

从上面的图2.1里面可以得出,事件分发步骤是:

  1. 判断当前ViewGroup的onInterceptTouchEvent是否拦截事件。
  2. 如果1里面onInterceptTouchEvent拦截了事件,那调用onTouchEvent,判断当前ViewGroup是否消费事件(这一步会先检查onTouchListener的onTouch函数的返回值,具体见下面的图2.2)。
  3. 如果1里面onInterceptTouchEvent没有拦截事件,那么调用child.dispatchTouchEvent去dfs判断(注意到这个函数是递归函数)。

也就是说,onInterceptTouchEvent决定的是,是否在当前ViewGroup就执行onTouchEvent。如果是false,它的child才有机会继续递归地执行dispatch;如果是true,那当前ViewGroup自己就执行onTouchEvent了,不再向下传递。

递归(DFS)!

再回头看看上面的执行顺序,霍然开朗了:


[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第6张图片
执行顺序

叶子结点的onTouchEvent返回了之后,才一层层向上继续执行父ViewGroup的onTouchEvent、dispatchTouchEvent。


优先级

onTouchListener优先级高于onTouchEvent:


[Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent_第7张图片
图2.2 onTouchListener的优先级高于onTouchEvent
//onTouchListener的优先级比onTouchEvent要高
public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
        ......
        return result;
    }

--

ref:
http://www.cnblogs.com/gyzboy/p/4518188.html
http://gundumw100.iteye.com/blog/1052270
《Android开发艺术探索》p141

你可能感兴趣的:([Android]dispatchTouchEvent, onTouchEvent和onInterceptTouchEvent)