网上介绍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方法
为了方便理解,我们缩减下代码,如下
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方法,同时使用其返回值~
通过上图上的源码执行顺序就知道为什么日志会这样输出了
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
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_DOWNgrandpa 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的机会券,用它才有资格买手机
拿例一做比较对象,下面开始整个类比流程~
A首先接到了这个信息,然后准备开始思考下这个劵的归属
(grandpa调用dispatchTouchEvent开始分发)A先想了一下是交给其他人呢?还是自己先用掉这个劵呢
(grandpa调用onInterceptTouchEvent判断是否拦截)A寻思暂时不拦截了吧,然后把劵给了B,让他去处理下这张劵
(grandpa不拦截,调用child.dispatchTouchEvent)B拿到劵后第一反应也是,我要自己用还是问有没有朋友要呢?
(parent调用onInterceptTouchEvent判断是否拦截)B也有点纠结,算了先问问有没有其他朋友要用吧,就给了C
(parent不拦截,调用child.dispatchTouchEvent给C分发)C拿到劵,额我没朋友,那就不问谁了,那我自己要不要用呢?
不用了最新穷~消费不起,那还给B吧。(child的分发就是看自己消费与否,返回false给B)
B一看,不要啊~ 那我自己要不要消费呢?还是不了,还给A吧
(parent调用super.dispatchTouchEvent,返回false给A)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的
好了,先总结一下
-
dispatchTouchEvent方法内容里处理的是分发过程。可以理解为从A->B->C一层层分发的动作
dispatchTouchEvent的返回值则代表是否将事件分发出去用掉了,自己用或者给某一层子级用都算分发成功。比如B把券用了,或者他发出去给的C把券用了,这两种情况下B的dispatchTouchEvent都会返回true给A -
onInterceptTouchEvent会在第一轮从父到子的时候在分发时调用,以它去决定是否拦截掉此事件不再向下分发。如果拦截下来,就会调用自己的onTouchEvent处理;如果不拦截,则继续向下传递
-
onTouchEvent代表消费掉事件。方法内容是具体的事件处理方法,如何处理点击滑动等。
onTouchEvent的返回值则代表对上级的反馈,通知这个东西我用掉啦,然后他的父级就会让分发方法也返回true
举了这个例子主要是为了说明分发、拦截、消费的流程,可以更具象化的理解,
这样我们再去用它去解释为什么例一、二中没有Up,而例三中有就更容易了
还是做个类比
我们的这个买手机其实是一套流程,用券之后还要支付余下的费用~
用券只是第一步,类似于Down
而支付余下的费用就类似于Up
结合到一起才是一个完整的行为
类似于一个Down+一个Up才是一次完整的点击
前俩例子里为什么没有Up呢,很好理解,
机会券啊!我都没用券呢没购买资格啊,有钱也没用啊!!!
所以例一二中既然没人用券,那自然也就不用考虑后续的购买行为了,因此只有Down,没Up
而一旦有人消费了,那后续的事件也就会来了
好,我们拿例三做类比,B消费掉了这个券
那么现在第二轮来了,销售员带着手机先跑来找A,听说有人要买是谁是谁~
这个流程依然是先从A开始分配
(grandpa.dispatchTouchEvent)A这个时候其实还可以不告诉销售员谁买的~
(grandpa.onInterceptTouchEvent 判断是否拦截)但是A还是没拦下来,告诉销售员是B买的
(grandpa不拦截,然后调用child.dispatchTouchEvent)销售员找到了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_DOWNgrandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP--- parent dispatchTouchEvent ACTION_CANCEL
--- parent onTouchEvent ACTION_CANCEL
前面Down行为和例三一样,后面就不同了
UP流程变了,然后多了个CANCEL的动作
这里我们可以理解为
售货员找到A问谁用的劵啊
(grandpa调用dispatchTouchEvent分发UP事件)A说我不告诉你!你就留我这吧!我得不到的(没券没资格买)别人也别想得到!!!
(grandpa调用onInterceptTouchEvent返回true,拦截UP)然后A告诉B,别等了孙砸!你的券没用啦!!!!
(parent调用dispatchTouchEvent分发CANCEL动作)然后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