触摸事件的分发(ViewGroup篇之一)

本篇和触摸事件的分发(View篇)将侧重于Android源码的分析。略显枯燥,但Read the fucking code的code不就是这样吗?

本文代码基于API15分析,而不是最新的API23。因API23中多出的代码和本文无关。
为减少篇幅完整地代码注释会在文末给出链接。

关键的成员变量

触摸目标

TouchTargetViewGroup的一个静态内部类。描述一个被触摸的 childView 和他所捕获的手指的ids。

private static final class TouchTarget {
 public View child;//被触摸到得child
 public int pointerIdBits;//由该目标捕获的所有的手指的IDS 的结合位掩码
 public TouchTarget next;//用于指向链表中的下一个TouchTarget
}

记录到的全是子View的信息,在处理向子View发送事件的逻辑时使用

触摸目标链表

为了有条理得向子View分发事件,ViewGroup需要记录所有用户触摸到的触摸目标信息。在经过一系列判断逻辑后,向其中的触摸目标分发事件。
这里是通过定义一个TouchTarget类型的成员变量mFirstTouchTarget来实现的,其记录了第一个触摸目标,是触摸目标链表的头。通过它(的next),可以找到链表中所有的触摸目标。

    private TouchTarget mFirstTouchTarget;

ViewGroup.dispatchTouchEvent()

ViewGroup中关于事件的分发是通过重写dispatchTouchEvent实现的。这部分是事件分发过程中最为复杂和难的地方。充分了解了这部分代码。将再也没有难点来阻挡你理解触摸事件的分发了。

ViewGroup中的dispatchTouchEvent()方法非常复杂,不了解整体设计思路,直接阅读将会一头雾水,云里雾里。所以,这里先把ViewGroup的的dispatchTouchEvent()用以下流程图归纳如下:

事件的传递之ViewGroup.png

其中比较关键的有以下几点:

  1. 触摸事件的安全策略
  2. 处理最初的DOWN事件
  3. 检查事件的拦截情况
  4. 检查是否取消
  5. 根据需要为DOWN事件更新触摸目标链表
  6. 分发触摸事件到目标View
  7. 根据需要,为UP、CANCEL事件更新触摸目标链表

其中,除了第一条相对分发流程比较独立外,其余都标有数字序号,并在图中用蓝色标注出来了。
接下来,我们分别详细讲每一个点。

〇、触摸事件的安全策略

根据用户体验来讲,用户只会尝试去点击可以直接看到的View(或ViewGroup),所以Google据此为触摸事件的分发制定了一个安全策略:
如果某View不处于顶部,并且View设置的属性是该View不在顶部时不响应触摸事件,则不分发该事件。

不满足安全策略需要同时满足两个条件:

  1. 配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
  2. 触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)

若不满足安全策略,onFilterTouchEventForSecurity(MotionEvent event)方法返回false,从上文流程图可以看到,这种情况会放弃接下来的所有分发操作。

    /**
     * 依据安全策略,过滤触摸事件。
     * @param event The motion event to be filtered. 需要被过滤的触摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

一、处理最初的DOWN事件,重置ViewGroup状态

Android触摸事件--MotionEvent一文我们可以知道:DOWN类型的事件是一系列触摸事件的开始。

  • 从用户角度讲:每次点击屏幕时,必然是首先触发一个DOWN事件。
  • 从代码角度讲:ViewGroup最先接收到得必然是DOWN事件,之后才会陆续接收到MOVE、UP或CANCEL等类型的事件。

当上一次触摸结束后,ViewGroup的某些状态可能已经发生了变化,比如ViewGroup的成员变量mFirstTouchTarget已经记录了一些值。
这时,做了两个动作:

  1. 取消并清空触摸目标链表。 cancelAndClearTouchTargets(MotionEvent event)
     /**
     * 取消并清空所有的的触摸目标
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) { //如果mFirstTouchTarget链表不为空,则清空该链表
            boolean syntheticEvent = false; //是否是我们人为合成的事件。
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);//人为合成一个 ACTION_CANCEL 事件
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                //从cancelAndClearTouchTargets的调用关系,我们可以发现,这里发送出去的事件只可能是两种:
                // 1、处理 DOWN 事件时的DOWN事件
                // 2、当该ViewGroup离开屏幕(Window)时,发送上面人为合成的ACTION_CANCEL 消息。
                // 但因为第二个参数为true,无论是哪种事件,最终都会被转化为取消事件。
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
2.png

根据Android中ViewGroup源码,该方法除了在这里之外,还会在ViewGroup的dispatchDetachedFromWindow()方法中被调用。这部分逻辑在流程图中特别标示出来了。在分发触摸事件的情况下地逻辑就简单了不少。
1)遍历触摸目标列表,将链表中的所有子View的 CANCEL_NEXT_UP_EVENT 标志全部重置
2)将取消事件传递给链表中所有的子View。(这里事件虽未DOWN事件,但dispatchTransformedTouchEvent方法第二个参数为true,最终都会被转化为ACTION_CANCEL事件)
3)清空触摸链表 clearTouchTargets()

  1. 重置所有触摸状态来为一个新的循环做准备。 resetTouchState();
    private void resetTouchState() {
        clearTouchTargets();//清空触摸链表
        resetCancelNextUpFlag(this);//重置CANCEL_NEXT_UP_EVENT 标志
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置 FLAG_DISALLOW_INTERCEPT 标志
    }

这里代码非常简单,就是清空触摸链表、重置本ViewGroup实例的 CANCEL_NEXT_UP_EVENT 标志、重置 FLAG_DISALLOW_INTERCEPT标志。
和cancelAndClearTouchTargets()方法主要处理触摸链表中的子View不同,该方法主要是针对ViewGroup实例自身的一些处理。

二、检查事件的拦截情况

APP开发工程师在开发程序时,可以对ViewGroup是否拦截事件做的限定有:
1、是否允许拦截触摸事件;由 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 来设定
2、是否要拦截某个触摸事件;由onInterceptTouchEvent(MotionEvent ev)返回值来决定

而ViewGroup内部是如何处理这些限定呢?
我们结合代码来看一下:

 @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    ****代码从略   ****
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//允许拦截
                    intercepted = onInterceptTouchEvent(ev);//根据onInterceptTouchEvent(dev)决定是否拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;//不允许拦截
                }
            } else {//不是down事件并且mFirstTouchTarget==null也没用与之对应的触摸目标
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                // 不存在触摸目标,并且该事件不是down事件,那么就继续拦截触摸事件。
                //(比如一个空的ViewGroup,则拦截触摸事件,通过自己的touchEvent处理)。
                // (又比如虽ViewGroup不为空,但触摸事件并没发生在任何子View上)。
                intercepted = true;
            }
    ****代码从略   ****
  }

在两种情况下,ViewGroup需要根据APP工程师的限定来决定事件是否被拦截:

  1. 当发生down事件时
    down事件是一个完整事件序列的的起点,当发生down事件时,这时还不知道是否有子View可消费该事件(此时mFirstTouchTarget已经被重置为null),必须根据APP工程师对ViewGroup的限定,方知是否需要拦截该事件。
  2. mFirstTouchTarget 不为null时
    这意味已经(靠该序列的起点down事件)找到要消费触摸事件的目标了,那么肯定不会是down事件了,而是move、up等类型的事件。这时,我们也需要根据APP工程师对ViewGroup的限定来决定是否拦截这种类型(move、up等)的事件。

从代码可知:只有在允许拦截并且onInterceptTouchEvent(MotionEvent ev)返回true时,才会对触摸事件拦截。

在以下情况下,触摸事件默认需要交给ViewGroup自己来处理,这时,我们可以当做事件被拦截了来处理:

  • 既不是down事件,此前也没有根据事件序列的down事件找到处理目标。(没有能够找到处理目标的move、up事件,拦截并交给ViewGroup自身来处理)。

比如说一个空的Layout布局;Layout布局不为空,但用户从未点击到任何子View上。

三、检查事件是否被取消

            final boolean canceled = resetCancelNextUpFlag(this)
                  || actionMasked == MotionEvent.ACTION_CANCEL;

很简单两种情况:

  1. 当前的View或ViewGroup要被从父View中detach时,PFLAG_CANCEL_NEXT_UP_EVENT就会被设为true;此时,resetCancelNextUpFlag(this)返回true,canceled被赋值为true,它就不再接受触摸事情。
  2. 触摸事件本身就是MotionEvent.ACTION_CANCEL类型的事件。

四、根据需要,为down事件更新触摸目标链表

  1. 获取到该触摸事件所对应手指ID,从触摸目标链表中清空与之对应的所有触摸目标。
  2. 遍历子View,直到找到即可接收触摸事件,触摸事件的坐标又坐落在其坐标范围内的childView;找到了就继续,找不到结束循环。
  3. 尝试从已有的触摸目标链表中找到与该childView对应的触摸目标实例,找到即结束。
  4. 没有在已有链表中找到对应的触摸目标实例,就把该事件发送给childView去处理,并生成一个触摸目标实例加入到触摸目标链表中。
  5. 找不到newTouchTarget,并且触摸目标链表不为空时,将newTouchTarget指向触摸目标链表的最初的target去处理

�1:第一个手指按下(单点触摸)时mFirstTouchTarget链表被清空,肯定找不到
2:多点触摸的后续手指按下时,从前面手指按下时产生的mFirstTouchTarget链表中寻找

五、分发触摸事件到目标。

如果触摸目标链表为空,直接把该ViewGroup当成一个普通的View来处理,

如果触摸目标链表不为空,则遍历链表,将事件分发给触摸目标对应的childView中去。若某childView事件接收到了CANCEL事件,就从链表中移出该触摸目标。

六、处理CANCEL事件和手指抬起事件

  • 该ViewGroup接收到了ACTION_CANCEL 事件,或者是最后一个手指抬起时
    重置所有触摸状态来为一个新的循环做准备:

      resetTouchState();
    
  • 多点触摸时,抬起了一根手指(非最后一个)
    从触摸链表中移出所有的与该手指有关的TouchTarget,不再考虑任何与该手指相关的操作(因为已经抬起)。

      final int actionIndex = ev.getActionIndex();
      final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
      removePointersFromTouchTargets(idBitsToRemove);
    

http://blog.csdn.net/ns_code/article/details/49848801
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/
http://blog.csdn.net/yanbober/article/details/45912661
http://blog.csdn.net/lfdfhl/article/details/42241253
http://www.cnblogs.com/hi0xcc/p/5583791.html

触摸事件的安全策略

    /**
     * Filter the touch event to apply security policies.
     * 依据安全策略,过滤触摸事件。
     * 安全策略:
     * ①:配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
     * ②:触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
     * @param event The motion event to be filtered. 需要被过滤的触摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

触发ACTION_CANCEL事件

运行上面两个例子,如果没什么差错的话,你是不会看到ACTION_CANCEL事件的,为什么呢?

要触发ACTION_CANCEL,就先得了解一个类ViewGroup,ViewGroup是一个放置其他views(子view)的特殊view,它是布局类(*Layout)、视图容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基类。

也就是说ViewGroup一般是做为父视图来容纳、管理其他子视图的。既然管理,在用户手势操作过程中,就会存在父视图不希望子视图响应用户手势操作的情况。Android提供了一个函数public boolean onInterceptTouchEvent (MotionEvent ev),在用户手势操作时,系统先调用父视图(一个继承自ViewGroup的类)的这个函数,来决定当前手势操作是由父视图还是子视图来响应、处理。我们仔细看看这个函数名,函数名中有一个单词intercept,经过查词典,这个单词的中文意思是拦截。在用户的一个完整手势操作过程中(起自ACTION_DOWN,终于ACTION_UP),对于每一次的MotionEvent``Android都会调用该函数,向父视图查询是否拦截当前MotionEvent,如果父视图返回false:不拦截,则系统会调用子视图的onTouchEvent函数;如果父视图返回true:拦截,则系统调用父视图的onTouchEvent。等等,有人不禁要问了,如果在这个完整手势操作过程中,父视图初期返回false、后期返回true会是一个什么样的情况呢(捣乱的来了)?这个嘛,是这个样子的,一开始返回false,毫无疑问,子视图会被调用onTouchEvent,但凡父视图在函数onInterceptTouch中有一次返回了true,那这一完整手势操作内所有后续的MotionEvent都会调用父视图的onTouchEvent,即使父视图后期反悔而改成返回false也不行(没有后悔药)。在这种父视图先返回false,后返回true的情况下,子视图收不到后续的事件,而只是在父视图由返回false改成返回true(拦截)的时候收到ACTION_CANCEL事件。

我们可以得到的结论

  • 对于一个事件序列,当ACTION_DOWN 事件被成功拦截时,那么对于剩下的一系列事件也会被拦截,并且不会再次执行onInterceptTouchEvent方法
  • 触摸目标链表只有在 ACTION_DOWNACTION_POINTER_DOWN事件时才可能被新增,所以,如果一系列事件的后续事件(ACTION_MOVE、ACTION_UP等)要想会被处理,这一系列事件的

疑问

  1. 在处理最初子View事件时,是否会对触摸链表中的子View都传递DOWN事件。

其他

本篇在将来的某天会有更新。

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

推荐阅读更多精彩内容