写在前面


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,结束!!!

更多相关文章

  1. Android事件总线之EventBus3.0基本使用
  2. Android(安卓)ImageView 的scaleType 属性图解
  3. Android手势ImageView之(自定义GestureDetector)
  4. 解决Android的ListView控件滚动时背景变黑(转)
  5. Androidの自定义进度条ProgressBar实现
  6. Android入门之Style与Theme
  7. Android入门第十六篇之Style与Theme
  8. Android帧布局
  9. Android中的控件

随机推荐

  1. Android中Adapter中edittext,checkbox记住
  2. Android(安卓)SharedPreferences应用解析
  3. android WebView总结
  4. Android如何查看应用签名信息
  5. Android这四个你不可不知的知识点,你都了
  6. android 笔记----禁止横屏和竖屏切换
  7. Android(安卓)USB Gadget复合设备驱动(打
  8. android cocos2d-x for Android安装和学
  9. Android多点触摸实现
  10. Android核心模块内容概述