onTouchEvent分发机制

Posted by boredream on March 27, 2017

网上介绍TouchEvent分发机制的文章很多,可能有的同学看了还是不明白 这里我会结合源码、画图、简化代码结构图、三个人买手机的类比等多个角度全面解释 其中用三个人买手机的例子做的类比,可以让你更具象化的直接理解整个流程

开始介绍事件分发机制之前,先简单介绍下这个TouchEvent是什么

安卓手机的交互,主要就是手指在屏幕上的戳戳滑滑点点 而我们的这些操作其实主要是由三种基本动作组成的:

  • 按下down
  • 移动move
  • 抬起up

安卓中把这个基础动作叫做TouchEvent

比如 一次点击就是按下、抬起组成的 一次长按就是按下、等待、抬起组成 一次滑动操作则是,按下、移动、抬起组成

其实除此之外还有多点触碰,光标操作等动作,这里暂时用不到,不讨论

安卓里经常会有多个控件重叠,即ViewGroup包含View的情况 这个时候点击到子View时,其实也是同时点到ViewGroup这个父控件的,那是把这个点击事件分给Parent呢还是Child呢? 这里我们就要了解下安卓中的TouchEvent事件分发机制啦

TouchEvent的分发传递主要涉及到三个核心方法

  • dispatchTouchEvent 分发Touch事件
  • onInterceptTouchEvent 拦截Touch事件
  • onTouchEvent 处理Touch事件

其中 onInterceptTouch是ViewGroup的方法。View中则没有该方法 dispatchTouchEvent在View和ViewGroup中有不同的实现,后面会展开介绍


那么在多层结构中TouchEvent到底怎么传递呢? 这仨方法用处和调用顺序是什么呢?

下面我们来撸个Demo实践下~ 【例一】 俩ViewGroup和一个View,方法全部默认不修改~ 嵌套布局

则当点击到Child上时,Touch事件的相关方法调用顺序就是

grandpa dispatchTouchEvent ACTION_DOWN grandpa onInterceptTouchEvent ACTION_DOWN — parent dispatchTouchEvent ACTION_DOWN — parent onInterceptTouchEvent ACTION_DOWN — — child dispatchTouchEvent ACTION_DOWN — — child onTouchEvent ACTION_DOWN — parent onTouchEvent ACTION_DOWN grandpa onTouchEvent ACTION_DOWN

为什么是这样一个从父级到子级再到父级的U型顺序呢? 其实看源码就知道啦,核心在于ViewGroup的dispatchTouchEvent方法 为了方便理解,我们缩减下代码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
boolean dispatchTouchEvent() {
    // 是否拦截
    boolean intercepted = onInterceptTouchEvent();

    if(!intercepted) {
        // 如果不拦截遍历所有child,判断是否有分发
        boolean handled;
        if (child == null) {
            // 等同于handled = onTouchEvent()
            handled = super.dispatchTouchEvent();
        } else {
            // 如果有child,再调用child的分发方法
            handled = child.dispatchTouchEvent();
        }

        if(handled) {
            touchTarget = child;
            break;
        }   
    }

    if(touchTarget == null) {
        // 如果所有child中都没有消费掉事件
        // 那么就把自己作为没child的普通View
        handled = super.dispatchTouchEvent();
    }

    return handled;
}

方法的作用是将屏幕点击事件向下(子一级)传递到目标控件上,或者传递给自己,如果自己就是目标的话

如果事件被(自己或者下面某一层的子控件)处理掉了的话,就返回true,否则返回false

那问题来了,如果我没有child了,或者我就是一个View,那我的dispatchTouchEvent返回值要如何获取呢? 这种情况下就会使用父类的dispatchTouchEvent方法, 也就是调用View类中的实现,简化代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean dispatchTouchEvent() {
    // 实质上就是调用onTouchEvent用其返回值
    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;
    }
    return result;
}

由此可见,只要是enable=false或者没有设置过touchListener, 那么他一定会调用onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值

这样看源码可能还是不太理解U型顺序 那我们把代码也按照上面的三层结构嵌套起来,就很好理解了,如下

例一

其中super.dispatchTouchEvent实际上就是调用了onTouchEvent方法,同时使用其返回值~ 通过上图上的源码执行顺序就知道为什么日志会这样输出了

  1. grandpa dispatchTouchEvent ACTION_DOWN
  2. grandpa onInterceptTouchEvent ACTION_DOWN
  3. — parent dispatchTouchEvent ACTION_DOWN
  4. — parent onInterceptTouchEvent ACTION_DOWN
  5. — — child dispatchTouchEvent ACTION_DOWN
  6. — — child onTouchEvent ACTION_DOWN
  7. — parent onTouchEvent ACTION_DOWN
  8. grandpa onTouchEvent ACTION_DOWN

dispatchTouchEvent分发的方法我们大概了解了, 那onInterceptTouchEvent拦截方法是做什么用的呢?

该方法用于拦截事件向下分发 当返回值为true时,就会拦截TouchEvent不再向下传递,直接交给自己的onTouchEvent方法处理。返回false则不拦截。

再做个试验 【例二】 把例一中的Parent层的onInterceptTouchEvent返回值改为true。 运行一下,点View,看下输出结果:

grandpa dispatchTouchEvent ACTION_DOWN grandpa onInterceptTouchEvent ACTION_DOWN — parent dispatchTouchEvent ACTION_DOWN — parent onInterceptTouchEvent ACTION_DOWN — parent onTouchEvent ACTION_DOWN grandpa onTouchEvent ACTION_DOWN

即当事件一层层向下传递到parent时,被他就拦截了下来然后自己消费使用。 再看一下源码中的执行顺序原理,如下图 例二

intercepted为true~ 没有进入条件,也就是图片里X的地方~ 就跳过了child.dispatchTouchEvent的向下事件分发了


最后还剩个onTouchEvent方法 方法的主体内容其实是处理具体操作逻辑的,是产生一次点击还是一次横纵向的滑动等

而他的返回值才会影响整个事件分发机制, 其意义在于通知父级的ViewGroup们是否已经消费找到目标Target了

同样,再试验一下 【例三】 只把例一中的Parent的TouchEvent返回值改为true。拦截方法不变 点一下View,则输出日志为

grandpa dispatchTouchEvent ACTION_DOWN grandpa onInterceptTouchEvent ACTION_DOWN — parent dispatchTouchEvent ACTION_DOWN — parent onInterceptTouchEvent ACTION_DOWN — — child dispatchTouchEvent ACTION_DOWN — — child onTouchEvent ACTION_DOWN — parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP grandpa onInterceptTouchEvent ACTION_UP — parent dispatchTouchEvent ACTION_UP — parent onTouchEvent ACTION_UP

暂时先看Down的逻辑,对应的源码执行顺序如下

例三

Down部分和例一的前7步流程都是一样的 但是例三源码图片中7的地方, parent调用super.dispatchTouchEvent实际上是调用了onTouchEvent方法, 这里因为我们修改成了true,所以dispatchTouchEvent最终也返回true。

所以返回到grandpa中,touchTarget 就非空了, 因此grandpa的onTouchEvent也没有执行~

Up部分我们后面再解释~

到这里我们就可以看出来 事件一旦被某一层消费掉,其它层就不会再消费了


好了,到这里其实对事件分发的机制就有个大概了解了 看了源码也知道里面的原理是怎么回事

但是 为什么例一二中没有Up,而例三中有呢? 为什么Up和Down的顺序不同呢? 为什么顺序是这样一个U型的呢? 看的我云里雾里的,光看源码和简单的demo还是太抽象了啊

为了方便理解,我们先来个具体事件的类比 事件的消费,就类似我们用了一个机会券,然后用它去买了一个手机 而事件的传递,就类似于这个机会券在不同朋友直接的流通传递

下面开始描述下这个传递的具体过程 有三个人ABC,之间的关系是A和B认识,B和C认识,但A和C不认识 某天A接到别人给它的一张购买iphone8的机会券,用它才有资格买手机

拿例一做比较对象,下面开始整个类比流程~

  1. A首先接到了这个信息,然后准备开始思考下这个劵的归属 (grandpa调用dispatchTouchEvent开始分发)
  1. A先想了一下是交给其他人呢?还是自己先用掉这个劵呢 (grandpa调用onInterceptTouchEvent判断是否拦截)
  1. A寻思暂时不拦截了吧,然后把劵给了B,让他去处理下这张劵 (grandpa不拦截,调用child.dispatchTouchEvent)
  1. B拿到劵后第一反应也是,我要自己用还是问有没有朋友要呢? (parent调用onInterceptTouchEvent判断是否拦截)
  1. B也有点纠结,算了先问问有没有其他朋友要用吧,就给了C (parent不拦截,调用child.dispatchTouchEvent给C分发)
  1. C拿到劵,额我没朋友,那就不问谁了,那我自己要不要用呢? 不用了最新穷~消费不起,那还给B吧。 (child的分发就是看自己消费与否,返回false给B)
  1. B一看,不要啊~ 那我自己要不要消费呢?还是不了,还给A吧 (parent调用super.dispatchTouchEvent,返回false给A)
  1. A拿回了转了一圈的劵,我手机也没坏啊也不买了~ (grandpa调用super.dispatchTouchEvent,返回false)

上面就是例一中1~8步骤的情况,所以最终输出的日志就是

grandpa dispatchTouchEvent ACTION_DOWN grandpa onInterceptTouchEvent ACTION_DOWN — parent dispatchTouchEvent ACTION_DOWN — parent onInterceptTouchEvent ACTION_DOWN — — child dispatchTouchEvent ACTION_DOWN — — child onTouchEvent ACTION_DOWN — parent onTouchEvent ACTION_DOWN grandpa onTouchEvent ACTION_DOWN

所有人都不消费劵,没分发出去。 其中步骤6 7 8中都调用了super.dispatchTouchEvent方法,上面我们介绍过, 这个方法内部实际上是调用的onTouchEvent方法~ 所以最后的输出日志顺序就是从父到子依次调用分发和拦截,然后从子到父依次调用消费。

而例二也是同理,区别在于 当B拿到券的时候,选择了拦截下来不再询问其他朋友了, 但是B又发现自己比较穷,所以也没消费,直接又还回给了A, A同样也不想要新手机也没有消费这个劵~ 所以最终的顺序就是,从A到B再返回A就结束了,没有经过C

例三的情况就不太一样了 当A->B->C传递到C时,C不消费又返回给了B,B一想别浪费了吧,决定消费掉了劵~ 相当于B这个parent调用了onTouchEvent消费方法,返回了true也就是用掉了它, 然后反馈给A说那个券我用了,就等于parent.dispatchTouchEvent返回true给上一级的A了, A听到消息后哦了一下~都用掉了,那自己也不用再去考虑用不用的事了 也就是A不会再调用grandpa.onTouchEvent方法了

到这里再回头看dispatchTouchEvent返回值的作用就更明确了 它的返回值其实是用于标志这个事件是否“用掉了”, 无论是我自己或者下面的子一级用掉了都算是用掉~

再比如这个例子中,如果我们让C消费掉事件, 那么B收到C的消息后,也会调用parent.dispatchTouchEvent返回true给A, 所以这个方法返回值的true是只要用掉就行,无论自己还是下面某一级, 而非我把事件传递下去就是true了,下面没人用最终其实还是返回false的

好了,先总结一下

  1. dispatchTouchEvent方法内容里处理的是分发过程。可以理解为从A->B->C一层层分发的动作 dispatchTouchEvent的返回值则代表是否将事件分发出去用掉了,自己用或者给某一层子级用都算分发成功。比如B把券用了,或者他发出去给的C把券用了,这两种情况下B的dispatchTouchEvent都会返回true给A
  2. onInterceptTouchEvent会在第一轮从父到子的时候在分发时调用,以它去决定是否拦截掉此事件不再向下分发。如果拦截下来,就会调用自己的onTouchEvent处理;如果不拦截,则继续向下传递
  3. onTouchEvent代表消费掉事件。方法内容是具体的事件处理方法,如何处理点击滑动等。 onTouchEvent的返回值则代表对上级的反馈,通知这个东西我用掉啦,然后他的父级就会让分发方法也返回true

举了这个例子主要是为了说明分发、拦截、消费的流程,可以更具象化的理解, 这样我们再去用它去解释为什么例一、二中没有Up,而例三中有就更容易了

还是做个类比 我们的这个买手机其实是一套流程,用券之后还要支付余下的费用~ 用券只是第一步,类似于Down 而支付余下的费用就类似于Up 结合到一起才是一个完整的行为 类似于一个Down+一个Up才是一次完整的点击

前俩例子里为什么没有Up呢,很好理解, 机会券啊!我都没用券呢没购买资格啊,有钱也没用啊!!!

所以例一二中既然没人用券,那自然也就不用考虑后续的购买行为了,因此只有Down,没Up

而一旦有人消费了,那后续的事件也就会来了 好,我们拿例三做类比,B消费掉了这个券 那么现在第二轮来了,销售员带着手机先跑来找A,听说有人要买是谁是谁~

  1. 这个流程依然是先从A开始分配 (grandpa.dispatchTouchEvent)
  1. A这个时候其实还可以不告诉销售员谁买的~ (grandpa.onInterceptTouchEvent 判断是否拦截)
  1. 但是A还是没拦下来,告诉销售员是B买的 (grandpa不拦截,然后调用child.dispatchTouchEvent)
  1. 销售员找到了B,B说没谁了,就是我了 (parent没有调用拦截方法) 然后B付钱结账尾款,完成了整个行为 (parent调用onTouchEvent返回true消费掉事件)

所以在例三中的Up顺序就是

grandpa dispatchTouchEvent ACTION_UP grandpa onInterceptTouchEvent ACTION_UP — parent dispatchTouchEvent ACTION_UP — parent onTouchEvent ACTION_UP

这次有了目标,所以不用再来个U型循环了,直接定位到目标B然后结束~ 那么这个目标是怎么个处理机制呢,我们会在后面详细解释~


回到例三,其实这里有个地方可以做点手脚的 就是在售货员上门找A的时候,A可以不告诉售货员B在哪~拦截下来

这次我们在例三的基础上进行修改,再整个试验 【例四】 在grandpa类的onInterceptTouchEvent中添加个判断, 如果动作是UP就return true拦截掉,DOWN则不拦截和之前一样

run下代码,看下输出日志

grandpa dispatchTouchEvent ACTION_DOWN grandpa onInterceptTouchEvent ACTION_DOWN — parent dispatchTouchEvent ACTION_DOWN — parent onInterceptTouchEvent ACTION_DOWN — — child dispatchTouchEvent ACTION_DOWN — — child onTouchEvent ACTION_DOWN — parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP grandpa onInterceptTouchEvent ACTION_UP

— parent dispatchTouchEvent ACTION_CANCEL — parent onTouchEvent ACTION_CANCEL

前面Down行为和例三一样,后面就不同了 UP流程变了,然后多了个CANCEL的动作 这里我们可以理解为

  1. 售货员找到A问谁用的劵啊 (grandpa调用dispatchTouchEvent分发UP事件)
  1. A说我不告诉你!你就留我这吧!我得不到的(没券没资格买)别人也别想得到!!! (grandpa调用onInterceptTouchEvent返回true,拦截UP)
  1. 然后A告诉B,别等了孙砸!你的券没用啦!!!! (parent调用dispatchTouchEvent分发CANCEL动作)
  1. 然后B也不用再考虑是否消费了,劵丢了吧~ (parent使用CANCEL动作调用onTouchEvent方法,结束)

当然,一般某层要用到事件时都会第一轮向下分发就拦截下来,然后用掉 所以例子三的情况比较少,不会那么无私的先问完所有朋友再考虑自己

而例四的情况也比较少,你要不用就一直不用,要用就直接拦截使用, 一般不会开始说不用~ 后来第二轮的时候又拦腰一刀大家一起死吧!!!的这么贱~


到这里其实大概也就了解的差不多了,还剩一个TouchTarget目标的概念, 为什么例三中Up和Down流程不同? 我们再回头去看完整点的源码~ 这次虽然也是省略代码,但是比之前的完善点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 1.每次起始动作就重置之前的TouchTarget等参数
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 2.如果是起始动作才拦截,或者已经有人消费掉了事件,再去判断拦截
            // 起始动作是第一次向下分发的时候,每个view都可以决定是否拦截,然后进一步判断是否消费,很好理解
            // 如果有人消费掉了事件,那么也拦截~ 就像例四中的情况,也可以再次判断是否拦截的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 3.这里可以设置一个disallowIntercept标志,如果是true,就是谁收到事件后都不准拦截!!!
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // 4.如果未拦截,只有Down动作才去子一级去找目标对象~
            // 因为找目标这个操作只有Down中才会处理
            if (actionMasked == MotionEvent.ACTION_DOWN ) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        newTouchTarget = getTouchTarget(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 5.把自己当做目标,去判断自己的onTouchEvent是否消费
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 6.如果有人消费掉了事件,找出他~
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                // 7.消费对象信息其实是一个链式对象,记载着一个一个传递的人的信息,遍历调用它child的分发方法
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                target = next;
            }
        }
    }

    return handled;
}

注意,有一个dispatchTransformedTouchEvent方法,内部简化代码为

1
2
3
4
5
6
7
8
9
10
11
12
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

其实就是判断如果没child了(是ViewGroup但是没子控件,或者自己就是View), 如果没child,就调用View的dispatchTouchEvent方法, 实质就是调用onTouchEvent判断是否消费掉事件 如果有child,就调用child的dispatchTouchEvent将事件一层层向下分发

例一二其实只用看之前的最简化源码就理解了~ 我们这里用这个比较完善的源码分析解释例三四中的复杂情况 其中关键主要在于多了一个TouchTarget的处理

其实我们在处理事件的时候,会在第一轮Down的时候先定位到目标,是谁消费了 然后在后续的Move、Up中,利用之前定位的信息更方便的找到目标,直接处理

从上面的源码中注释2代码的位置我们可以看出来, 第一次Down的时候我们才会去判断是否拦截,或者有目标的时候才拦截 因为第一次传券的时候可以拦截,而如果没人用券也就是没有目标那第二轮就不用拦截了,都买不了手机

如果有人消费呢,比如例三中parent消费掉了事件 那么上面源码就会在Down时,进入到注释4代码的位置,去child一层层找到目标, 当找到某层onTouchEvent返回true消费掉事件的对象后,就会调用addTouchTarget记录下这个目标 那么第二轮UP到来时,就会进入注释2代码条件,再判断是否拦截,例三中是不做拦截 再往下运行,因为不是Down,所以不会进入注释4代码的判断条件 到最后,就会在注释5和6代码中二选一,例三里是B消费了,有目标,所以进入条件6, 然后在注释7代码处用dispatchTransformedTouchEvent方法,将Up直接向下层层传递给目标

向下传递的核心主要是在于dispatchTransformedTouchEvent方法 第一轮动作的Down时,只要不拦截,就会在注释4代码处遍历所有child调用该方法层层传递下去 而后续其他动作时,就会进入注释6代码条件,然后遍历TouchTarget中的信息用该方法层层分发

但是要注意不要误解 第一次Down的时候会for循环所有child,因为A可能有多个朋友B1、B2、B3。。。他会挨个问谁要券啊~ 所以第二轮Up的时候也会while(target.next)的迭代循环挨个判断~但是next是遍历同级,不是子级 dispatchTrancformTouchEvent(target.child)这里的.child才是向子一级一层一层分发传递的地方

这个TouchTarget对象,主要保存的是传递路线信息,它是一个链式结构 不过这个路线不是A->B->C的一个单子,而是ABC每个人都会保存一个向下的路线信息

比如例子三中B用了券,反馈给了A~ 那么A这里就会保存一个A->B的信息,就是从我这里去找目标B 如果把例一中修改成C消费掉事件,那么A就会保存一个A->B,然后B中还会保存一个B->C的信息, 这样销售员来找A的时候,如果A不拦截,就会顺着A->B的信息找到B,再顺着B手里的B->C信息找到C 当找到最后一个对象的时候,发现C手里没有下一个目标的路线信息了,那你就是目标没跑了~

Cancel部分就不解释了,dispatchTrancformTouchEvent中会判断,如果cancel=true动作, 则会把动作改成ACTION_CANCEL一层一层的传下去~ 其他还有一些不拦截标志、id什么的设置细节就不介绍了,下面可以自己阅读下源码巩固完善下, 当然我暂时也没达到每一行代码都完全掌握的地步,如果文章有不合适的地方欢迎指正和共同讨论~