快捷搜索:

可以让你更具象化的直接理解整个流程

网上介绍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,方法全部默认不修改~

图片 1嵌套布局

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

grandpa dispatchTouchEvent ACTION_DOWNgrandpa onInterceptTouchEvent ACTION_DOWN--- parent dispatchTouchEvent ACTION_DOWN--- parent onInterceptTouchEvent ACTION_DOWN--- --- child dispatchTouchEvent ACTION_DOWN--- --- child onTouchEvent ACTION_DOWN--- parent onTouchEvent ACTION_DOWNgrandpa 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 { 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 { result = true; } return result;}

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

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

图片 2例一

其中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_DOWNgrandpa onInterceptTouchEvent ACTION_DOWN--- parent dispatchTouchEvent ACTION_DOWN--- parent onInterceptTouchEvent ACTION_DOWN--- parent onTouchEvent ACTION_DOWNgrandpa onTouchEvent ACTION_DOWN

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

图片 3例二

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

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

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

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

grandpa dispatchTouchEvent ACTION_DOWNgrandpa 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_UPgrandpa onInterceptTouchEvent ACTION_UP--- parent dispatchTouchEvent ACTION_UP--- parent onTouchEvent ACTION_UP

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

图片 4例三

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_DOWNgrandpa onInterceptTouchEvent ACTION_DOWN--- parent dispatchTouchEvent ACTION_DOWN--- parent onInterceptTouchEvent ACTION_DOWN--- --- child dispatchTouchEvent ACTION_DOWN--- --- child onTouchEvent ACTION_DOWN--- parent onTouchEvent ACTION_DOWNgrandpa 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)

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

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

  4. 销售员找到了B,B说没谁了,就是我了(parent没有调用拦截方法)然后B付钱结账尾款,完成了整个行为(parent调用onTouchEvent返回true消费掉事件)

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

grandpa dispatchTouchEvent ACTION_UPgrandpa 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_DOWNgrandpa 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_UPgrandpa 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流程不同?我们再回头去看完整点的源码~ 这次虽然也是省略代码,但是比之前的完善点

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity { if (actionMasked == MotionEvent.ACTION_DOWN) { // 1.每次起始动作就重置之前的TouchTarget等参数 cancelAndClearTouchTargets; 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.setAction; } 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; 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代码~

本文由澳门新葡萄京8455官网发布于澳门新葡萄京8455官网,转载请注明出处:可以让你更具象化的直接理解整个流程

您可能还会对下面的文章感兴趣: