窥探Android Touch事件内幕系列之二

上一篇文章我们主要介绍了Android UI事件处理机制-基于监听器方式、基于回调方法,同时从View的角度分析了Touch事件分发流程。这一篇文章我们将从ViewGroup角度来分析Touch事件分发流程,之前计划的关于Robolectric如何测试相关事件分发流程将会在后续『单元测试之-自定义View』中进行介绍。

事件传递流程

Touch事件在Android中对应的就是MotionEvent类。对Touch事件的分发其实就是对MotionEvent对象进行分发。当我们在屏幕上进行一次点击操作时,MotionEvent就产生了,Android系统会将这个MotionEvent对象传递到一个具体的View进行处理,这个传递过程就是事件分发。Android事件分发是一种委托思想:上层委托下层,父容器委托子元素处理。

下图是Android系统的界面结构图:最顶层为Activity的ViewGroup,下面有若干的ViewGroup节点,每个节点之下又有若干的ViewGroup节点或者View节点,依次类推。当一个Touch事件到达根节点,即Acitivty的ViewGroup时,该Touch事件会被依次分发,分发过程就是调用子View(ViewGroup)的dispatchTouchEvent方法实现的。简单来说,就是ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。下图中Touch事件下发顺序是这样的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断。在上述例子中如果⑤的dispatchTouchEvent返回结果为true,那么⑥-⑦-③-④将都接收不到本次Touch事件。(这段节选自:Android:30分钟弄明白Touch事件分发机制

Android View结构模型

ViewGroup继承自View,ViewGroup中对触摸事件的处理,很多也都继承于View。但是,ViewGroup又有自己对触摸事件的特定处理。ViewGroup重载了dispatchTouchEvent()方法,新增了onInterceptTouchEvent()方法。上一篇文章中我们已经分析过View类的事件处理机制-dispatchTouchEvent、onTouchEvent,本篇将会分析ViewGroup的dispatchTouchEvent分发流程。

ViewGroup->dispatchTouchEvent流程

关于ViewGroup->dispatchTouchEvent已经有很多大神们进行了详细的源码分析,我这里给出一些链接,方便大家去参考

1、Android 触摸事件机制(四) ViewGroup中触摸事件详解

2、 13.View的事件分发机制——dispatchTouchEvent详解

3、Android ViewGroup事件分发机制

为节约篇幅这篇文章就不贴源码了,将以精心烹制的『流程图』作为�主菜,另配上香甜可口的甜点『解析』为大家呈现ViewGroup中dispatchTouchEvent这道饕餮盛宴。

ViewGroup->dispatchTouchEvent整体流程图
  • Down操作:首先会通过拦截机制判断是否需要拦截,如果拦截,则不进行子View的Down操作分发,直接由当前ViewGroup处理;反之则循环遍历子View,进行事件分发。当然循环遍历所有子View之后也可能存在没有子View处理该Down操作,这个时候会继续交给当前ViewGroup处理。
  • Up、Move操作:也是先通过拦截机制判断是否需要拦截,如果拦截,则由当前ViewGroup直接处理;反之则分发给处理Down操作的子View进行处理。

流程图中涉及到的拦截机制、TouchTarget、MotionEvent、Down操作分发流程等等,会在下面的内容中为大家一一讲解。

拦截机制

在自定义ViewGroup中,有时候需要实现Touch事件拦截,比如ListView下拉刷新就是典型的Touch事件拦截的例子。Touch事件拦截就是在Touch事件被父 view拦截,不会分发给其child,即使触摸发生在该child身上。被拦截的事件会转到parent view的onTouchEvent方法中进行处理。

那么ViewGroup的拦截机制具体原则是什么呢?我们先来看下流程图,方便大家理解。

ViewGroup->dispatchTouchEvent->拦截机制流程图.png

这里根据流程图总结下具体原则:

  • ViewGroup有一个禁止拦截的标志位:FLAG_DISALLOW_INTERCEPT,如果调用requestDisallowInterceptTouchEvent(),该标志位为True,则禁止该ViewGroup拦截事件。
  • ViewGroup新增的接口onInterceptTouchEvent(),默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这样该事件就不会往子View传递了。
  • Down操作-Touch事件的开始,此时ViewGroup会先根据FLAG_DISALLOW_INTERCEPT标志位判断,如果允许拦截,则进一步调用新增接口onInterceptTouchEvent()来确定Down操作是否继续传递给子View。
  • Move、UP操作-如果mFirstTouchTarget != null(Down操作已经被某个子View消费掉了),此时,才有必要再进一步判断当前ViewGroup是否需要对Move、UP进行拦截,具体如何判断同Down操作。
  • Move、UP操作-如果mFirstTouchTarget == null(Down操作已经被当前ViewGroup拦截了,或者遍历了所有子View 但都没有对Down操作进行处理),此时,完全没必要进一步判断当前ViewGroup是否要拦截,因为这种情况Down操作肯定已经由该ViewGroup了,后续的Move、UP自然也由该ViewGroup处理。

TouchTarget

TouchTarget是ViewGroup的一个内部类,是一个触摸对象的链表类。ViewGroup类中mFirstTouchTarget就是当前ViewGroup中触摸对象链表的头节点,用于记录处理某Down操作的所有子View和触摸点(对于多点触摸,需要记录每次的触摸点)信息。下面给出了这个类的源码,方便大家理解。

 /* Describes a touched view and the ids of the pointers that it has captured.
     *
     * This code assumes that pointer ids are always in the range 0..31 such that
     * it can use a bitfield to track which pointer ids are present.
     * As it happens, the lower layers of the input dispatch pipeline also use the
     * same trick so the assumption should be safe here...
     */
    private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(View child, int pointerIdBits) {
            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }

MotionEvent

这篇文章一开始就介绍了MotionEvent是事件源(Button、CheckBox、EditText等)产生的Touch事件。我们需要从MotionEvent中获取哪些信息呢?

  • 事件类型

    • 常见的事件类型有: ACTION_DOWN: 表示用户开始触摸、 ACTION_MOVE: 表示用户手指移动、ACTION_UP:表示用户抬起了手指 、ACTION_POINTER_DOWN:有一个非主要的手指按下、ACTION_POINTER_UP:一个非主要的手指抬起来。后两者是在Android 2.2支持多点触摸之后增加的事件类型。
    • 获取事件类型的方法:getActionMasked()
  • 事件触摸点索引信息

    • Android是支持多点触控,通过触摸点索引信息可以得知一个MotionEvent事件类型是哪个触摸点触发。
    • 获取触摸点索引信息的方法:getActionIndex()
  • 事件发生的位置信息

    • getX()方法获得事件发生时,触摸的中间区域在屏幕的X轴
    • getY() 获得事件发生时,触摸的中间区域在屏幕的Y轴
    • 多点触摸还可以通过getX(int pointerIndex) 和 getY(int pointerIndex)来获取对应手指事件的X、Y轴信息
    • 事件发生的位置信息的坐标系是Android系统坐标系(这个概念可以参考文章:Android中的坐标系以及获取坐标的方法

Down操作处理流程

当Touch事件MotionEvent中的ACTION_DOWN、ACTION_POINTER_DOWN操作来临时ViewGroup的分发流程是如何的呢?请先看流程图,我们再来分析。

ViewGroup->dispatchTouchEvent->Down操作处理流程.png
  • 首先我们需要判断当前事件是否是取消事件、是否已经被ViewGroup拦截,如果是取消事件或者已经被拦截,那么该Down操作是没有必要在子View中进行事件分发,则直接跳出该流程。
  • 核心是循环遍历当前ViewGroup的所有子View。
  • 循环过程中第一步是判断子View是否可接受Touch事件,同时当前的Touch事件的位置是否位于子View中。如果满足这两个条件才会进一步对该子View和Touch事件进行关联、将Touch事件分发给该子View;如果不满足这两个条件,则说明当前Touch事件和该子View没有任何联系,直接退出当前循环,进行下一个子View的处理。
  • 如何将子View和当前Touch事件进行关联呢?TouchTarget链表粉墨登场(噔噔噔。。。)通过mFirstTouchTarget链表中获取和当前子View相关的TouchTarget,如果已经存在该子View相关的TouchTarget,直接更新该TouchTarget的pointerIdBits属性,让其包含当前触摸点信息,同时退出循环遍历子View的流程。(例如:第一个手指触摸在View - A上,这个时候第二个手指也触摸在View - A上)如果不存在该子View相关的TouchTarget,则新建一个并插入到mFirstTouchTarget链表中,同时调用该子View 的dispatchTouchTarget方法,这里的流程就回归到我们上一篇中关于View的事件处理流程。根据子View的dispatchTouchTarget返回值判断子View是否消费了当前Touch事件,若消费则退出循环遍历子View的流程,反之则继续遍历。

Up、Move操作处理流程

ViewGroup主要是先通过对Down操作进行分发,记录处理Down操作的子View链表,然后循环遍历该链表,完成Up、Move操作的分发处理。

ViewGroup->dispatchTouchEvent->Up/Move操作处理流程

总结

  • ViewGroup的dispatchTouchEvent负责事件分发,由上至下,有父容器至子View依次分发
  • 如果ViewGroup找到了能够处理该事件的子View,则直接交给子View处理,自己的onTouchEvent不会被触发
  • 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
  • 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其事件的拦截
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容