Android事件分发之dispatchTouchEvent()

写在前面


Android里的事件处理是很复杂但也是非常重要的,一般看别人写的开源控件里,基本上都会涉及到对事件的处理,所以理解这一部分是非常有必要的。曾经看过很多关于Android事件处理的文章,很多都是围绕着onInterceptTouchEvent()和onTouchEvent()两个方法来说的,一般的解释是说Android的View层次结构是递归的,如果这些方法返回了true就代表消费了事件,如果返回false,就把事件传递给它的父亲。我很迷惑这些事件是怎样从子控件传递到父控件的,难道当中做了什么特殊处理?反正看例子程序是越看越迷糊。最后还是拿到了一份源码,以及找到了几篇不错的文章,才把这些疑惑给消除掉。


我相信现在有很多人也是对这一块的知识点很迷惑,写这篇文章也算把我看到的一些和自己理解的一些分享给大家,而一些别人已经写过的我也不再重复了,别人写的也比我写的详细多了。下面就是我推荐的几篇文章,如果大家希望很好的了解这一部分,这几篇文章非常值得一读。


Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

Andriod 从源码的角度详解View,ViewGroup的Touch事件的分发机制


希望大家在阅读下面的内容前,可以先阅读这几篇文章。


dispatchTouchEvent()


大家都知道如果自定义控件涉及到事件处理,一般都会重写onInterceptTouchEvent()和onTouchEvent()这两个方法,所以大家最关心的也是这两个方法,但其实dispatchTouchEvent()这个方法才是整个事件处理中最为重要的方法,特别是ViewGroup中的dispatchTouchEvent()方法。下面我就结合源码分析一下这个方法,其实很多内容在上面三篇文章中已经涉及到了。

阅读源码之前,我们先了解下下面两个知识点:
  • 一个事件就是从ACTION_DOWN开始,而结束于ACTION_UP,我们分析事件的各个动作时,需要把ACTION_DOWN动作与其他的动作区分开来,因为我们必须通过消费ACTION_DOWN动作来声明到底是哪个View对这次的事件感兴趣,即找到处理这次事件的View
  • target view的概念:上面所指的处理这次事件的View我们可以把它称为target view。但这也是相对的。举个例子,有这样一个布局:Activity -> layout1 -> layout2 -> button,他们是互相嵌套的。如果最终接收事件的是button,那么对整个事件来说,它的target view就是button,而对于layout2来说,它的target view是button,对layout1来说,它的target view是layout2。一个事件要从layout1传到button,不可能直接给button,而是需要通过layout2来传递,因为layout1没有这么长的手,够不着。(其实这是因为layout1是layout2的parent view,它直接包含了layout2,所以也只能接触到layout2,而同样的道理,layout2可以接触到button)

ViewGroup中dispatchTouchEvent()方法的源码(Android 2.2版本中的源码):
public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;

        // 默认为false,即默认是允许拦截事件的
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
        	/**
        	 * 对于每一次的点击事件(down-move-...-move-up),
        	 * 它都会去找一个接收事件的target view,而且此次找到的target view
        	 * (如果能找到的话)不会影响下一次点击事件的target view,因为在每次事件
        	 * 的一开始,都将target view置为null了嘛!
        	 */
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            // 如果我们的ViewGroup不允许拦截事件,或者没有成功拦截下来,就执行到if语句里面的内容
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
            	// 万一在onInterceptTouchEvent()中将这个action给变了呢,是吧?
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
                
                /**
                 * 也只有在ACTION_DOWN事件的时候,才会对该ViewGroup的所有子View进行
                 * 一一探测,在其他事件的时候,会直接根据target view来处理事件,所以一个
                 * View有可能接收到ACTION_DOWN事件,但不一定能接收到ACTION_MOVE或者
                 * ACTION_UP事件。这一点要切记!!!
                 */
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                            	/**
                            	 * OK,找到了target view,即有view接收并处理了该事件.
                            	 * 注意:这里的target view是该ViewGroup的直接子View,
                            	 * 而该ViewGroup也会成为它父ViewGroup的target view.
                            	 * 
                            	 * 所以,我们在执行ACTION_DOWN事件时候,就已经确定了该次
                            	 * 事件的target view.
                            	 */
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
        	// 恢复默认值,即允许ViewGroup拦截掉事件,所以我们如果在执行一次事件之前
        	// 设置了不允许拦截事件,不用担心它会影响下一次事件的执行。
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        /**
         * 如果执行到这里的事件是:
         * 1. ACTION_DOWN事件:
         *    如果target view为null,则说明该ViewGroup的所有子View都没法成为target view,
         *    那么就试试该ViewGroup自身能否成为target view。ViewGroup的父类是View,所以
         *    这里super.dispatchTouchEvent(ev)就是调用View类中onTouchEvent()等方法。所以
         *    如果该ViewGroup设置了TouchListener并且返回true,或者在onTouchEvent()中返回
         *    true,那么该ViewGroup就成为了target view。
         *    此时对于该ViewGroup的Parent View来说,它正在执行的是上面的for循环,如果它
         *    发现该ViewGroup成为了target view,那么循环就终止了,否则继续循环。
         *    
         * 2. ACTION_MOVE或ACTION_UP事件,这里会出现三种情况:
         *    a. 所有的ACTION_MOVE和ACTION_UP事件执行到这的时候target view都为null,说明
         *       该ViewGroup就是整个事件的target view,交给它处理这些事件即可。
         *    b. 与上面相反,所以事件执行到这的时候target view都不为null,我们就需要把事件
         *       交给当前ViewGroup的target view来处理。
         *    c. 参考下面的onInterceptTouchEvent()方法,即在确定了target view的情况下,该
         *       ViewGroup把事件给拦截下来了,会先向当前ViewGroup的target view发送一个
         *       ACTION_CANCEL动作,然后设置target view为NULL,所以以后的事件都交给当前
         *       ViewGroup本身来执行。
         */
        final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);
        }
        
        /**
         * 如果是ACTION_DOWN动作,执行到这里一定会执行完了。
         * 接下来的处理只有ACTION_MOVE、ACTION_UP等动作能执行到了
         * (这都是在该ViewGroup的target view不为null前提下执行的)
         */
        
        // if have a target, see if we're allowed to and want to intercept its
        // events
        // 如果我们设置了允许拦截事件,并且成功拦截下来了
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            /*
             * 既然当前的ViewGroup把事件拦截下来了,那么它的target view就不应该再收到
             * 这个事件,我们给target view发送一个CANCAL事件。
             */
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            // 注意这里:清空了当前ViewGroup的target view,但是对于它的父View或者整个事件来说,
            // target view 仍然是存在的。
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        // 一般的情况,我们直接把这个事件传递给我们的target view,既然有这个传递过程,
        // 那么肯定是经过了当前ViewGroup的onInterceptTouchEvent()的,这从上面代码
        // 中也可得知。
        // 并且,此时target view的返回值并不会对整个事件造成影响了。
        return target.dispatchTouchEvent(ev);
    }


这里面的注释已经很详细了,我还是把几个地方简单分析一下(其中英文注释是源码中本来就有的):

分析之前,我们假设有这样一个布局,Activity -> layout1 -> layout2 -> button,button是我们最终接收事件的控件,而我们正处在layout2的源码中。

这里源码比较长,大家可以开两个窗口。首先我们看到源码12行,事件开始时发出ACTION_DOWN动作,会进入这个if语句中。到第30行,因为是ViewGroup,所以会对这个事件进行拦截,顾名思义,如果事件被拦截下来了,它还会被传递给当前ViewGroup的子控件吗?当然不会了。但是如果没被拦截,我们也就进入了这个if语句中。来到47行,这里是在遍历当前ViewGroup的所有子控件,根据触摸条件判断子控件是否能接收到该事件,如果可以,就把这个事件传递过去,这个传递体现在58行,我们看到,这里调用的其实就是View的dispatchTouchEvent()方法,如果这里返回true,OK,那说明这个子控件接收并消费了这个动作,也就是说当前ViewGroup找到了target view,68行是记录下这个target view,以后可以直接用,而对当前ViewGroup的父View来说,也找到了target view,所以69行中直接返回true,我们可以想象在这个父View中也会记录下这个target view,并且返回true。假如我们的事件在ACTION_DOWN的时候被拦截下来了,或者所有的子控件都没有消费这个事件,会出现什么情况呢?来到114行,这里显然会进入这个if语句中,最后直接返回super.dispatchTouchEvent(ev),也就是说,如果当前ViewGroup的所有子控件都不接收事件,那么就看看当前ViewGroup自身是否能处理这个事件(看来父亲还是先为孩子着想的)。如果这里返回true,那么我们可以想象当前ViewGroup的父View会记录下target view并且返回true,如果返回false,父View会去查找下一个孩子。

我们再用上面的布局来理清一下这个过程,我们正处于layout2中,显然layout1正处在上面的那个for循环中,如果layout2不拦截事件,那么layout2就会把事件传递给button,如果这个button接收了该事件,那么layout2的for循环结束,layout1的for循环也结束,并且都返回true,此时layout2的target view是button,layout1的target view是layout2。如果button不接收事件,或者layout2把这个事件给拦截下来了,那么我们就看看layout2自己能否处理这个事件,如果能处理,那么layout2的dispatchTouchEvent()方法结束,layout1的for循环结束,此时layout2的target view为null,layout1的target view为layout2,如果layout2不能处理这个事件,那么layout1的for循环继续执行,layout2再也不会和本次事件有任何关系了。

接下来看ACTION_MOVE和ACTION_UP事件。可以分为两种情况考虑:
  1. button为接收事件的target view。事件传递过来的时候肯定还是要经过layout2。这时候114行的target肯定不会为空,直接到135行。这里也是拦截事件,所以只要网子控件传递事件,都会经过父控件的拦截的,除非设置了父控件不拦截事件,默认都是拦截的。我们先假设这里事件没有被拦截下来,会直接到179行,也就是直接把事件传递给子控件,即事件在button中被处理。如果事件在135行被拦截下来了,也就是说layout2把事件拦截下来了,我们可以看到139行和145行,我们会给target view也就是button发送一个ACTION_CANCEL事件,在156行直接返回true。这里特别要注意152行,当前ViewGroup的target view被清空了。如果是第二次ACTION_MOVE动作来了会出现什么情况呢?现在这一次的动作会直接进入115行的if语句中去,也就是这次的动作交给当然ViewGroup自己来处理了。以后的动作也是一样。
  2. 如果layout2为target view。其实和上面被拦截后的情况一样,事件会由layout2自己来处理。当前如果layout2不是target view,事件都不会进来了,这就不用考虑了。
这里就不写例子了,找到一篇文章,里面有示例和结果,大家可以对照来检验自己对事件传递的理解。点击这里

总结


  1. 一个事件总是以ACTION_DOWN开始,以ACTION_UP结束。
  2. 一个事件开始于Activity的dispatchTouchEvent()方法,并向下传递到各层布局中,传递过程中可以通过ViewGroup的onInterceptTouchEvent()将事件拦截下来,如果未找到消费事件的View,事件会向上传递,并依此调用各层View的onTouchEvent()方法,最后未被消费的事件会传递到Activity的onTouchEvent()方法中。
  3. View的OnTouchListener也可以消费事件。
  4. 关于返回值。onInterceptTouchEvent()的返回值代表事件是否会被拦截下来,当然前提是我们的控件允许拦截事件,可以通过ViewGroup的requestDisallowInterceptTouchEvent()方法控制。dispatchTouchEvent()和onTouchEvent()的返回值决定着事件是否会被继续执行。如在ACTION_DOWN中,如果某个控件的dispatchTouchEvent()方法返回true,那么ACTION_DOWN事件就不会传递到其他任何控件了。而在ACTION_MOVE或者ACTION_UP中返回true,那么事件就不会传递到Activity中去了,否则会被Activity的onTouchEvent()方法接收到。一般onTouchEvent()方法的返回值决定着dispatchTouchEvent()方法的返回值。

其他参考资料


这是一个外国的讲解事件机制的视频,但是全英文的,我没看懂。这是视频地址:点击打开链接
但是视频上带的PPT对事件机制的总结,我感觉非常到位,也非常简洁,很值得一看,由于PPT的原地址已经失效了,我已经上传到了CSDN了。这是地址:下载地址


OK,结束!!!

你可能感兴趣的:(Android)