浅尝安卓事件分发机制

本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。

本文简单介绍安卓应用层的事件分发机制,并辅以案例进行分析。

视频版教程:http://v.youku.com/v_show/id_XMTY5MjczMjE3Ng==.html。

0. 前言

授人以鱼不如授人以渔。

安卓系统源码是深入学习安卓开发的首选资料,原汁原味,营养丰富。
而且大部分源码带有注释,具有很强的可读性,你只需要战胜的自己的畏惧心理然后 read the fucking source code。

关于阅读源码,我有两个小建议:

1. 选择合适的源代码版本
从 2008 年 9 月份发布的 1.0 版,到 2015 年 10 月份的 6.0,部分源码发生了很大变化。

以应用层的事件分发机制为例,2.3.3 版本的源码仅仅处理单点触控,而在 6.0 版本则具有完善的多点触控。相应的,后者的代码比前者更为复杂,但思路是一脉相承的。

所以,如果首次学习事件分发机制,建议从低版本,如 2.2 或 2.3 版本开始,先了解单点触控触控的流程,然后再过度到最新版本的多点触控,这样学习曲线相对平滑,降低难度。

阅读 2.x 版本的源码,可以参考郭霖大牛的博客《 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)》;更高版本的源码,可以参考这两篇的注释:《Android Touch事件传递机制全面解析(从WMS到View树)》 和 《Android事件传递之子View和父View的那点事》。

2. 单步调试的重要性
学习源代码,仅仅通过阅读和思考是远远不够的,而且容易陷入死胡同。动手尝试是不可或缺的。
在 OnTouchLisnter.onTouch() 和 OnClickListener.onClick() 方法中打印日志,观察日志输出的时机和顺序,这是一种很直观的方法。
单步调试也是很有效的尝试方法,通过观察不同案例下(如设置监听事件和未设置监听事件)源码的执行路线和中间变量的赋值,我们往往有种茅塞顿开、豁然开朗的感觉。阅读和调试,一静一动,相得益彰。

为了避免发生断点失效或执行顺序混乱等诡异情况,我们要做到如下两点:

  1. 调试需要使用 Nexus 真机/模拟器(Genymotion 或 SDK 自带的都行),因为这些机型的 Rom 版本是原生的,与源代码一致;
  2. AndroidStudio 工程的 build.gradle 里面的 compileSdkVersion 要与上述 Rom 版本号一致;例如,如果 build.gradle 文件的 compileSdkVersion=23, 那么在 Nexus 5(6.0系统,版本号 23)真机或模拟器上做单步,才能完美匹配(如果工程中有多个 module,切记每个 module 都符合这一点,否则你会发现实际单步调试的源码是版本最低的 module 对应的源码)。

其实,知道以上两点之后,大家完全自己去单步调试了。
以下内容都是基于本人阅读源码和单步调试和日常经验总结得出的。

1. 基础知识

1.1 直观认识

先来看个效果图:


浅尝安卓事件分发机制_第1张图片

假如上述效果图的布局实现方式如下(省略具体属性值):

<FrameLayout>
   <ImageView />    
   <TextView />
FrameLayout>

父View 和子 View 是以“叠罗汉”的方式放在一起的,就像 HTML 里的 CSS(Cascading Style Sheet,层叠样式表)一样。二者通过“父子”关系联系起来,事件也是通过这种联系顺藤摸瓜来寻找“目标”View 的,处于上层的子 View 后于父 View 接收事件,但先于父 View 处理事件(如果愿意消费的话)。


浅尝安卓事件分发机制_第2张图片
View 层叠关系示意图

1.2 相关代码

本文基于安卓 SDK 23 版本源代码。
View.java 和 ViewGroup.java 中与事件分发相关的几个方法:

  • View.dispatchTouchEvent(),true 表示事件被消费,否则返回 false;
  • View.onTouchEvent(),true 表示事件被消费,否则返回 false;
  • ViewGroup.dispatchTouchEvent(),true 表示事件被消费,否则返回 false;
  • ViewGroup.onInterceptTouchEvent(),true 表示事件被拦截,否则返回 false;

继承关系如下:


浅尝安卓事件分发机制_第3张图片

由上图可以看出,ViewGroup 没有重写 onTouchEvent() 方法,仅仅是原样继承(但是 TextView 中对该方法进行了惨无人道的重写)。而且,在 ViewGroup 中见到 super.dispatchTouchEvent() 时一定要记得这是 ViewGroup 把自己当成普通的 View 了,也就是在调用 View.dispatchTouchEvent() 方法。

“手势”的定义:以 ACTION_DOWN 开始,以 ACTION_UP 结束的一连串触摸事件;触摸事件的类型可以分为:

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP
  • ACTION_CANCEL
  • ACTION_POINTER_DOWN
  • ACTION_POINTER_UP

    其中,带 POINTER 的类型是多点触控特有的,我们可以认为一个 pointer 就是一个手指。

2. 点击之后,发生了什么?

以下只讨论单点触控。
本文的 demo 布局是一个 RelativeLayout(后面会用 ScrollView 代替),里面有两个 TextView。我们假设红色的 TextView 上绑定了一个 OnClickListener。在我们自己的布局外面,系统还会再套几层布局,也就是虚线以上的部分。由于尺寸原因,后面的事件消费示意图中我们将省略部分系统层级。


浅尝安卓事件分发机制_第4张图片
完整的 View 树的层级示意图

这里解释一下 mFirstTouchTarget 这个成员变量。

每个 ViewGroup 实例中都有 mFirstTouchTarget。mFirstTouchTarget 的类型是 TouchTarget,TouchTarget 是一个链表节点的数据结构,每个 TouchTarget 实例里面封装着 mFirstTouchTarget 变量所在实例的一个子 View(也可能是 ViewGroup,以后统称为子 View),表示能接收事件的子 View。

当打头阵的 DOWN(以下将 ACTION_DOWN 简写为 DOWN,对 ACTION_MOVE、ACTION_UP、ACTION_CANCEL 做同样处理) 事件被某个子 View 消费时,mFirstTouchTarget 就指向该子 View,然后后续事件(MOVE / UP)到来时就直奔该子 View 供其消费;而当没有子 View 消费 DOWN 事件时,后续事件到来时,顶层的 DecorView.mFirstTouchTarget 为 null,DecorView 就直接调用 super.dispatchTouchEvent(event) 处理它们,而不再下发了。

单点触控时,mFirstTouchTarget 指向的链表最多只有一个节点;多点触控时可能会有多于一个节点。

关于 DOWN 和 MOVE / UP 事件执行过程,可参见下面代码的注释。

// more code 

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;

                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {

                        // DOWN
                        handled = true;

                    } else {

                        // MOVE and UP
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

// more code 

2.1 存在监听事件

DOWN 之后发生了什么?
DOWN 事件是手势的开始,好似一个侦察兵,任务是检查有没有子 View 愿意消费它及后续事件,如果有做好标记(TouchTarget)。


浅尝安卓事件分发机制_第5张图片
DOWN 就像一个侦察兵

上层的 ViewGroup 都很“谦虚”,收到 DOWN 事件后,以类似递归的方式询问自己的子 View 是否愿意消费。从顶层的 DecorView 开始,每个 ViewGroup 遍历自己的所有子 View,遍历的顺序与其添加子 View 的顺序相反。找出被点击到的子 View 后,调用其 dispatchTouchEvent() 方法,如果该子 View 绑定了监听事件(如果是 OnTouchListener,其 onTouch() 方法必须返回 true;如果在 OnTouchListener.onTouch() 里面做了处理,但是返回 false,仍然认为是未消费),其 dispatchTouchEvent() 返回 true,然后标记该该子 View:将其封装成 TouchTarget 对象,并将 mFirstTouchTarget 指向该对象;如果以被点击到的 View 为根节点的 View 树没有找到接收该事件的子 View,同样做标记: mFirstTouchTarget = null,alreadyDispatchedToTouchTarget = false。

标记的作用是后续事件到来时,就不用再遍历寻找被击中的子 View 了,而是通过标记直接找到该子 View。

MOVE / UP 之后发生了什么?
后续事件直接交给每个 ViewGroup 的 mFirstTouchTarget 里面的子 View,调用子 View 的 dispatchTouchEvent() 方法,直到红色的 TextView 消费了这些事件。


浅尝安卓事件分发机制_第6张图片
红色 TextView 上绑定监听事件时的事件消费过程

2.2 没有监听事件

假设所有的 ViewGroup 或 View 上都没有绑定监听事件(或者绑定了 OnTouchListener,但是其 onTouch() 返回 false),红色 TextView 上也没有任何监听事件。DOWN 事件一直被各级 ViewGroup 谦虚的传递到最底层的 TextView,这时连 TextView 也不愿消费之,于是其 dispatchTouchEvent() 返回false,然后 TextView 的父 View 接着执行自己的 dispatchTouchEvent(),走到 OnTouchListener.onTouch() 和 onTouchEvent() 里面,当然这两个都返回 false;然后 TextView 的父 View 的 dispatchTouchEvent() 方法执行完毕,返回到了 TextView 的父 View 的父 View 的dispatchTouchEvent() 方法中,然后执行其 OnTouchListener.onTouch() 和 onTouchEvent() 里面……

最后,各个层级的 mFirstTouchTarget = null,ViewGroup.dispatchTransformedTouchEvent() 方法的 child 参数为 null,每个 ViewGroup 都被当做 View 处理,从最底层开始,依次调用 View.dispatchTouchEvent() 方法(最终调用是 View.onTouchEvent() 方法),直至 Activity.dispatchTouchEvent()。

对于后续的事件,由于 DecorView.mFirstTouchTarget = null,调用dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),被 DecorView 的父类即 View.dispatchTouchEvent() 方法消费。


浅尝安卓事件分发机制_第7张图片

2.3 ViewGroup 拦截后续事件

我们把 RelativeLayout 换成可以上下滑动的 ScrollView,其子 View 是一个 TextView,TextView 绑定了监听事件。

如果给 TextView 的背景设置了按压态,我们按住 TextView 不松手也不滑动时,我们能看到背景变色,说明 DOWN 确实被 TextView 消费了。

仍然保持手指的按压状态,然后上下滑动,ScrollView 开始滚动,说明 MOVE 被 TextView 的父 View 即 ScrollView 消费了,这时 TextView 会接收到 CANCEL 事件。


浅尝安卓事件分发机制_第8张图片
声称对 DOWN “感兴趣”但是后续事件被父 View 拦截的子 View 会收到 CANCEL 事件

3. 应用案例

3.0 案例零: 优化层级的重要性

从上面的分析可以看出,不管有没有设置监听事件,每次点击,都会触发从 View 树的自上而下的单路径遍历,层级越多遍历耗时越多,响应时间越大,用户体验越差。从这个角度,也可以说明布局层级优化的重要性。关于布局优化,请移步《 布局优化技巧笔记》。

还说明,没事别闲的蛋疼在屏幕上乱摸乱点,虽然屏幕没什么反应,但是屏幕后面是有代码在空跑的,当然也在耗电。

3.1 案例一:不同种类监听事件的优先级

如果给某个 View 同时绑定 OnClickListener,OnTouchListener,TouchDelegate(关于 mTouchDelegate 的使用参见《用 TouchDelegate 扩大子 View 的点击区域》)三个事件(简直丧心病狂),那么它们的执行顺序是怎样的呢?

这时我们需要查看 View.dispatchTouchEvent() 方法,看里面是怎么处理这些监听事件的:

 public boolean dispatchTouchEvent(MotionEvent event) {
        // some code

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }
// some code

        return result;
    }


public boolean onTouchEvent(MotionEvent event) {

        // some code

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

// some code 

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

// more code
}

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

// more code 
    }

我们可以看到,现在 View.dispatchTouchEvent() 方法中执行了 mOnTouchListener,然后进入 View.onTouchEvent() 方法,依次执行了 mTouchDelegate.onTouchEvent(event) 和 performClick(),后者执行了 OnClickListener.onClick(this)。

所以,三者的执行顺序为: OnTouchListener,TouchDelegate,OnClickListener。

同时,我们应当注意到在 View.onTouchEvent() 方法的注释:

 // A disabled view that is clickable still consumes the touch
 // events, it just doesn't respond to them.

即,同时满足 disabled 和 clickable = true 的 View 会消费触摸事件,但不会有任何反应。

3.2 案例二:如何更好的绑定监听事件?

回头看下这个例子:


浅尝安卓事件分发机制_第9张图片

<FrameLayout>
   <ImageView />    
   <TextView />
FrameLayout>

需求:点击图片和数字具有相同的响应点击事件(如页面跳转等)。
这种情况下,只给 FrameLayout 绑定监听事件即可,虽然 DOWN 事件会下发到 ImageView 和 TextView,询问它们是否愿意消费,被拒绝后 DOWN 事件向上传递,依次经过 ImageView或TextView(具体是谁取决于点击坐标落在哪个的区域范围内) 和 FrameLayout 的 onTouchEvent() 方法,最后被 FrameLayout.onTouchEvent() 消费。后续事件将直接被 FrameLayout.onTouchEvent() 消费,而不再下发;


浅尝安卓事件分发机制_第10张图片
事件消费过程示意图

如果能继承 FrameLayout(假设为 MyFrameLayout),并重写 MyFrameLayout 的 onInterceptTouchEvent() 方法:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

这样 MyFrameLayout 直接拦截点击事件进行消费,不再下发给 ImageVIew 和 TextView,此时的事件消费过程为:


浅尝安卓事件分发机制_第11张图片
事件消费过程示意图

如果不在 FrameLayout 上绑定,而是给 ImageView 和 TextView 绑定,所有的事件都会经过 FrameLayout 继续下发给ImageView 或 TextView,方法调用层数增加,相应时间延长;

如果在 FrameLayout、ImageView 和 TextView 上同时绑定事件,则根据事件的落点进行判断。例如,落在 ImageView 范围内的事件(当然也落在了 FrameLayout 范围内)只触发 ImageView 的事件;落在 FrameLayout 范围内但是既未落在 TextView 范围内也未落在 ImageView 范围内的事件,才会由 FrameLayout 进行消费。这种效果与我们的直观认识一致的。

3.3 案例三:瀑布流效果

这是一个经典案例(虽然在实际开发中很少见到),通过重写 ViewGroup.onInterceptTouchEvent() 方法和 ViewGroup.onTouchEvent() 方法来控制事件的分发过程,详见该博客《Android事件分发机制练习—打造属于自己的瀑布流》。

3.4 案例四:ClickableSpan 的 Bug

请移步《TextView ClickableSpan 事件触发的坑》。

3.5 案例五:优雅的隐藏 PopupWindow

3.6 案例六:左划露出删除按钮

3.7 案例七:优雅的隐藏输入框和软键盘

4. 更多好文

  • Mastering the Andrdoid Touch System
  • 《 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)》
  • 《Android Touch事件传递机制全面解析(从WMS到View树)》
  • 《Android事件传递之子View和父View的那点事》
  • Android笔记:触摸事件的分析与总结—-TouchEvent处理机制
  • Android事件传递机制
  • Android Touch事件派发流程源码分析
  • Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch)
  • Handling single and multi touch on Android - Tutorial
  • Android Touch and Multi-touch Event Handling
  • Android Development - What I wish I had known earlier

本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。

你可能感兴趣的:(Android,UI,开发,Android,UI,开发)