TouchEvent事件分发机制全解析

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

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

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

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

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

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

其实除此以外还有多点触碰,光标操做等动做,这里暂时用不到,不讨论源码分析

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

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

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

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


那么在多层结构中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
  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开始分发)

  2. A先想了一下是交给其余人呢?仍是本身先用掉这个劵呢
    (grandpa调用onInterceptTouchEvent判断是否拦截)

  3. A寻思暂时不拦截了吧,而后把劵给了B,让他去处理下这张劵
    (grandpa不拦截,调用child.dispatchTouchEvent)

  4. B拿到劵后第一反应也是,我要本身用仍是问有没有朋友要呢?
    (parent调用onInterceptTouchEvent判断是否拦截)

  5. B也有点纠结,算了先问问有没有其余朋友要用吧,就给了C
    (parent不拦截,调用child.dispatchTouchEvent给C分发)

  6. C拿到劵,额我没朋友,那就不问谁了,那我本身要不要用呢?
    不用了最新穷~消费不起,那还给B吧。
    (child的分发就是看本身消费与否,返回false给B)

  7. B一看,不要啊~ 那我本身要不要消费呢?仍是不了,还给A吧
    (parent调用super.dispatchTouchEvent,返回false给A)

  8. 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

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

仍是作个类比
咱们的这个买手机实际上是一套流程,用券以后还要支付余下的费用~
用券只是第一步,相似于Down
而支付余下的费用就相似于Up
结合到一块儿才是一个完整的行为
相似于一个Down+一个Up才是一次完整的点击

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

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

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

  1. 这个流程依然是先从A开始分配
    (grandpa.dispatchTouchEvent)

  2. A这个时候其实还能够不告诉销售员谁买的~
    (grandpa.onInterceptTouchEvent 判断是否拦截)

  3. 可是A仍是没拦下来,告诉销售员是B买的
    (grandpa不拦截,而后调用child.dispatchTouchEvent)

  4. 销售员找到了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事件)

  2. A说我不告诉你!你就留我这吧!我得不到的(没券没资格买)别人也别想获得!!!
    (grandpa调用onInterceptTouchEvent返回true,拦截UP)

  3. 而后A告诉B,别等了孙砸!你的券没用啦!!!!
    (parent调用dispatchTouchEvent分发CANCEL动做)

  4. 而后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可能有多个朋友B一、B二、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代码~
github.com/boredream

相关文章
相关标签/搜索