android开发andnroidAndroid

TouchEvent事件分发机制全解析

2017-03-27  本文已影响2861人  boredream

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

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

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

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

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

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

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

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

其中
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方法
为了方便理解,我们缩减下代码,如下

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类中的实现,简化代码如下

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
  1. grandpa onInterceptTouchEvent ACTION_DOWN
  2. --- parent dispatchTouchEvent ACTION_DOWN
  3. --- parent onInterceptTouchEvent ACTION_DOWN
  4. --- --- child dispatchTouchEvent ACTION_DOWN
  5. --- --- child onTouchEvent ACTION_DOWN
  6. --- parent onTouchEvent ACTION_DOWN
  7. 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流程不同?
我们再回头去看完整点的源码~ 这次虽然也是省略代码,但是比之前的完善点

@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方法,内部简化代码为

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什么的设置细节就不介绍了,下面可以自己阅读下源码巩固完善下,
当然我暂时也没达到每一行代码都完全掌握的地步,如果文章有不合适的地方欢迎指正和共同讨论~

最后宣传一下个人的Github账号,有多个不错的开源项目哟~
欢迎follow我和star代码~
https://github.com/boredream

上一篇下一篇

猜你喜欢

热点阅读