前言
上一期跟随拇指记者,发现了Android公司
在指派具体的人之前的种种机制,今天就继续探索,看看任务具体的处理消费逻辑。
交给做任务具体的人(ViewGroup)
开始分派任务,也就是ViewGroup
的事件分发时间,这部分内容是老生常谈了,最重要的就是这个dispatchTouchEvent
方法。
假设我们没有看过源码,那么 事件来了,会产生多种传递拦截的可能,我画了个脑图:
其中产生的疑问就包括:
ViewGroup
是否拦截事件,拦截后怎么处理?- 不拦截后交给
子View
或者子ViewGroup
怎么处理? 子View
怎么决定是否拦截?子View
拦截后怎么处理事件?子View
不拦截事件后父元素ViewGroup
怎么处理事件?ViewGroup
不拦截,子View
也不拦截,最终事件怎么处理?
接下来就具体分析分析。
ViewGroup是否拦截事件,拦截后怎么处理?
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //1 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); } } //2 if (!canceled && !intercepted) { //事件传递给子view } //3 if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } } private boolean dispatchTransformedTouchEvent(View child) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } }
上述代码分成了三部分,分为ViewGroup
是否拦截、拦截后则不再传递下去,ViewGroup
拦截后的处理。
1、ViewGroup是否拦截
可以看到,初始化了一个变量intercepted
,代表viewGroup
是否拦截。
如果满足两个条件任意一个,才去讨论ViewGroup是否拦截:
- 事件为
ACTION_DOWN
,也就是按下事件。 mFirstTouchTarget
不为null
其中mFirstTouchTarget
是个链表结构,代表某个子元素成功消费了该事件,所以mFirstTouchTarget不为null就代表有子view消费事件,这个待会再细谈。当第一次进入这个方法,事件肯定就是ACTION_DOWN
,所以就进入了if语句,这时候获取了一个叫做disallowIntercept(不允许拦截)的变量,暂且按下不表,接着看。然后给这个intercepted赋值为onInterceptTouchEvent
方法的结果,我们可以理解为 viewGroup是否拦截取决于onInterceptTouchEvent方法。
2、拦截后则不再传递
如果viewGroup拦截了,也就是intercepted
为true,自然也就不需要再往子view或者子ViewGroup进行传递了。
3、ViewGroup拦截后的处理
如果mFirstTouchTarget
为null,则表示没有子View进行拦截,然后就转向执行dispatchTransformedTouchEvent
方法,代表ViewGroup要自己再进行一次分发处理。
这里有个问题就是为什么不直接判断intercepted
呢?非要去判断这个mFirstTouchTarget
?
- 因为
mFirstTouchTarget==null
不仅代表ViewGroup要自己消费事件,也代表了ViewGroup
没消费并且子View
也没有去消费事件,两种情况都会执行到这里。
也就是ViewGroup
拦截或子View没有拦截,都会调用到dispatchTransformedTouchEvent
方法,在该方法中,最后会调用super.dispatchTouchEvent
。
super代表ViewGroup的父类View,也就是ViewGroup会作为一个普通View执行View.dispatchTouchEvent
方法,至于这个方法具体做了什么,待会和View的事件处理再一起看。
通过上面的分析,我们可以得出ViewGroup
拦截的伪代码:
public boolean dispatchTouchEvent(MotionEvent event) { boolean isConsume = false; if (isViewGroup) { if (onInterceptTouchEvent(event)) { isConsume = super.dispatchTouchEvent(event); } } return isConsume; }
如果是ViewGroup,会先执行到onInterceptTouchEvent
方法判断是否拦截,如果拦截,则执行父类View的dispatchTouchEvent
方法。
ViewGroup不拦截后交给子View或者子ViewGroup处理?
接着说ViewGroup
不拦截的情况,也就会传到子View的情况:
if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; //1 if (newTouchTarget == null && childrenCount != 0) { for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //2 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //3 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } }
ViewGroup
不拦截,则intercepted
为false,那么就会进入上述的if语句中。
同样分为三部分来说,分别是遍历子View,判断事件坐标,传递事件
1、遍历子View
第一部分就是遍历当前ViewGroup所有的子View。
2、判断事件坐标
然后会判断这个事件是否在当前子View的坐标内,如果用户触摸的地方都不是当前的View自然不需要对这个view在进行分发处理,还有个条件就是当前View没有在动画状态。
3、传递事件
如果事件坐标在这个View内,就开始传递事件,调用dispatchTransformedTouchEvent方法,如果为true,就调用addTouchTarget方法记录事件消费链。
dispatchTransformedTouchEvent方法是不是有点熟悉?没错,刚才也出现过,再看一遍:
private boolean dispatchTransformedTouchEvent(View child) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } }
这里对传进来的 child
进行了判断,这个child
就是子View
,如果子View不为null,就调用这个子View的dispatchTouchEvent
方法,继续分发事件。如果为null,就是刚才的情况,调用父类的dispatchTouchEvent
方法,默认为自己来消费事件。
当然,这个child有可能为viewGroup有可能为View,总之就是继续分发调用子View
或者子ViewGroup
的方法。
到此,一个关于dispatchTouchEvent
的递归就显现出来了:如果某个ViewGroup无法消费事件,那么就会传递给子view/子ViewGroup的dispatchTouchEvent方法,如果是ViewGroup,那么又会重复这个操作,直到某个View/ViewGroup消费事件。
最后,如果dispatchTransformedTouchEvent
方法返回true,就代表有子view消费了事件,然后会调用到addTouchTarget
方法:
在该方法中,会对mFirstTouchTarget
这个单链表进行了赋值,记录消费链(但是在单点触控的情况下,其单链表的结构并没有用上,只是作为一个普通的TouchTarget对象,待会会说到),然后就break退出了循环。
接下来就看看关于View内部具体处理事件的逻辑。
子View怎么处理事件,是否拦截?
public boolean dispatchTouchEvent(MotionEvent event) { if (onFilterTouchEventForSecurity(event)) { 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; }
其实就是两个逻辑:
- 1、如果View设置了
setOnTouchListener
并且onTouch
方法返回true,那么onTouchEvent
就不会被执行。 - 2、否则,执行
onTouchEvent
方法。
所以默认情况下是直接会执行onTouchEvent
方法。
关于View的事件分发我们也可以写一段伪代码,并且增加了setOnClickListener
方法的调用:
public void consumeEvent(MotionEvent event) { if (!setOnTouchListener || !onTouch) { onTouchEvent(event); } if (setOnClickListener) { onClick(); } }
子View拦截后怎么处理事件?
子View拦截后,就会给单链表mFirstTouchTarget
赋值。
这个刚才已经说过了。逻辑就在addTouchTarget方法中,我们来具体看看:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } public static TouchTarget obtain(@NonNull View child, int pointerIdBits) { final TouchTarget target; target.child = child; return target; }
这个单链表到底怎么连的呢?之前我们说过dispatchTouchEvent
是一个递归的过程,当某个子View消费了事件,那么通过addTouchTarget
方法,就会让mFirstTouchTarget
的child值指向那个子View,依此向上,最后就会拼接成一个类似单链表结构,尾节点就是消费的那个View。
为什么说类似呢?因为mFirstTouchTarget
并没有真正连起来,而是通过每个ViewGroup的mFirstTouchTarget
间接连起来。
打个比方,我们假设一个View树关系:
A / \ B C / \ D E
A、B、C为ViewGroup,D、E为View。
当我们触摸的点在ViewD中,事件分发的顺序就是A-C—D
。
在C遍历D的时候,ViewD消费了事件,所以走到了addTouchTarget方法中,包装了一个包含ViewD的TouchTarget
,我们叫它TargetD。
然后设置C的mFirstTouchTarget
为TargetD,也就是其child值为ViewD。
再返回上一层,也就是A层,因为D消费了事件,所以C的dispatchTouchEvent
方法也返回了true,同样调用了addTouchTarget
方法,包装了一个TargetC。
然后会设置A的mFirstTouchTarget
为TargetC,也就是其child值为ViewC。
最终的分发结构就是:
A.mFirstTouchTarget.child -> C
C.mFirstTouchTarget.child -> D
所以说mFirstTouchTarget
通过child找到了消费链的下一层View,然后下一层又继续通过child找到下下层View,依次往下,就记录了消费的完整路径。
那mFirstTouchTarget
的链表结构用到哪了呢?多点触控。
对于多点触控且点击目标不同的情况,mFirstTouchTarget
才会作为链表结构存在,next指向上一个手指按下时创建的TouchTarget对象。
而在单点触控情况下,mFirstTouchTarget
链表会蜕变成单个TouchTarget
对象:
mFirstTouchTarget.next
始终为null。mFirstTouchTarget.child
赋值为这条消费链的下一层View,一层层递归调用每一层的mFirstTouchTarget.child,直到消费的那个view。
最后再补充一点,每次ACTION_DOWN事件来到的时候,mFirstTouchTarget就会被重置,迎接新的一轮事件序列。
子View不拦截事件后ViewGroup怎么处理事件?
子View不拦截事件,那么mFirstTouchTarget
就为null,退出循环后,调用了dispatchTransformedTouchEvent
方法。
//3 if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
最终调用了super.dispatchTouchEvent
,也就是View.dispatchTouchEvent
方法。
可以看到子View
不拦截事件和ViewGroup
拦截事件的处理是一样的都会走到这个方法中。
那么这个方法到底干了什么呢?上面说到View的处理方法dispatchTouchEvent
已经说过了,还是那段伪代码,只不过在这里View是作为ViewGroup的父类。
所以,小结一下,如果所有子View
都不处理事件,那么:
- 默认执行
ViewGroup
的onTouchEvent
方法。 - 如果设置
ViewGroup
的setOnTouchListener
,就会执行onTouch
方法。
ViewGroup不拦截,子View也不拦截,最终事件怎么处理?
最后一点,如果ViewGroup
不拦截,子View
也不拦截,这个意思就是mFirstTouchTarget == null
的同时,dispatchTransformedTouchEvent
方法也返回false。
总之,就是所有ViewGroup的dispatchTouchEvent
方法都返回false,这时候该怎么处理呢?返回到一开始大佬会谈的时候:
//Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
没错,如果superDispatchTouchEvent
方法返回false,那么就会执行Activity的onTouchEvent
方法。
小结
小结一下:
- 事件分发的本质就是一个递归方法,通过往下传递,调用
dispatchTouchEvent
方法,找到事件的处理者,这也就是项目中常见的责任链模式
。 - 在消费过程中,ViewGroup的处理方法就是
onInterceptTouchEvent
- 在消费过程中,View的处理方法就是
onTouchEvent
方法。 - 如果底层View不消费,则一步步往上执行父元素的
onTouchEvent
方法。 - 如果所有View的
onTouchEvent
方法都返回false,则最后会执行到Activity的onTouchEvent
方法,事件分发也就结束了。
完整事件消费伪代码:
public boolean dispatchTouchEvent(MotionEvent event) { boolean isConsume = false; if (isViewGroup) { //ViewGroup if (onInterceptTouchEvent(event)) { isConsume = consumeEvent(event); } else { isConsume = child.dispatchTouchEvent(event); } } else { //View isConsume = consumeEvent(event); } if (!isConsume) { //如果自己没拦截,子View没有消费,自己也要调用消费方法 isConsume = consumeEvent(event); } return isConsume; } public void consumeEvent(MotionEvent event) { //自己消费事件的逻辑,默认会调用到onTouchEvent if (!setOnTouchListener || !onTouch) { onTouchEvent(event); } }
dispatchTouchEvent() + onInterceptTouchEvent() + onTouchEvent()
,大家也可以把这三个方法作为理解记忆事件分发的重点。
后续任务处理(事件序列)
终于,任务找到了它的主人,看似流程也结束了,但是还存在一个问题就是,这个任务之后的后续任务该怎么处理呢?比如要增加某某模块功能。
不可能再走一遍公司流程吧?如果按照正常逻辑,是应该找到当初负责我们任务的那个人来继续处理,看看Android公司
是不是这么做的。
一个MotionEvent
事件序列一般包括:
ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
刚才我们都说的是ACTION_DOWN
,也就是手机按下的事件处理,那么后续的移动手机,离开屏幕事件该怎么处理呢?
假设之前已经有一个ACTION_DOWN
并且被某个子View消费了,所以mFirstTouchTarget
会有一条完整的指向,这时候来了第二个事件——ACTION_MOVE
。
if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { }
然后就会发现,ACTION_MOVE
事件根本进不去对子View
的循环方法,而是直接到了最后面的逻辑:
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } predecessor = target; target = next; } }
如果mFirstTouchTarget
为null,就是之前说过的转到ViewGroup自身的onTouchEvent
方法。
这里很明显不为null,所以走到else中,又开始遍历mFirstTouchTarget
,之前说过单点触控的时候,target.next
为null,target.child
为消费链的下一层View,所以其实就是将事件交给了下一层View。
这里有个点很多朋友可能之前没注意到,就是当ACTION_DOWN
的时候,走到这里,会通过mFirstTouchTarget找到那个消费的View执行dispatchTransformedTouchEvent
。但是这之前,遍历View
的时候已经执行了一次dispatchTransformedTouchEvent
方法,难道这里还要执行一次dispatchTransformedTouchEvent
方法吗?这不就重复了?
- 这就涉及到另一个变量
alreadyDispatchedToNewTouchTarget
。这个变量代表之前是否已经执行过一次View消费事件,当事件为ACTION_DOWN
,就会遍历View,如果view消费了事件,那么alreadyDispatchedToNewTouchTarget
就被赋值为true,所以到这里也就不会再次执行了,直接handled = true
。
所以后续任务
的处理逻辑也基本明白了:
只要某个View开始处理拦截事件,那么这一整个事件序列都只能交给它来处理。
优化任务派发流程(解决滑动冲突)
到此,任务终于是分发完成了,任务完成后,小组开了一个总结会议
:
其实任务分发过程还是有可以优化的过程,比如有些任务是不一定就只交给一个人做,比如交给两个人做,把A擅长的任务给A做,B擅长的任务给B做,最大化利用好每个人。
但是我们之前的逻辑默认
是按下任务交给了A,后续都会交给A。所以这时候就需要设计一种机制对某些任务进行拦截。
其实这就涉及到滑动冲突
的问题了,举例一个场景:
外面的ViewGroup
是横向移动,而内部的ViewGroup
是需要纵向移动的,所以需要在ACTION_MOVE
的时候对事件进行判断和拦截。(类似ViewGroup+Fragment+Recyclerview)
直接说Android公司的解决方案,两种方案:
- 外部拦截法。
- 内部拦截法。
外部拦截法
外部拦截法比较简单,因为不管子View是否拦截,每次都会执行onInterceptTouchEvnet
方法,所以我们就可以在这个方法中,根据自己的业务条件选择是否拦截事件。
//外部拦截法:父view.java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; //父view拦截条件 boolean parentCanIntercept; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; } return intercepted; }
逻辑很简单,就是根据业务条件,在onInterceptTouchEvent
中决定是否拦截,因为这种方法是在父View中控制是否拦截,所以这种方法叫做外部拦截法。
但是这和我们之前的认知又冲突了,如果ACTION_DOWN
交给了子View处理,那么后续事件应该会直接被分发给这个view呀,为什么还能被父View拦截的?
我们再来看看dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent ev) { final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { intercepted = onInterceptTouchEvent(ev); } // Dispatch to touch targets. if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { while (target != null) { if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } } } }
当事件为ACTION_MOVE
的时候,并且在onInterceptTouchEvent
方法返回了true,所以这里的intercepted=true
,再到下面的逻辑,cancelChild
的值也为true,然后被传到了dispatchTransformedTouchEvent
方法,没错,又是这个方法,不同的是cancelChild
子段为true。
看这个字段的名字肯定是和取消子view事件有关的,继续看看:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } }
看出来了么,当第二个字段cancel为true的时候,事件会被修改成ACTION_CANCEL
!!,然后才会被继续传下去。
所以就算某个View消费了ACTION_DOWN
,但是当后续事件来的同时,在父元素的onInterceptTouchEvent()
中返回true,那么这个事件就会被修改为ACTION_CACLE
事件再传给子View。
所以子View
再次交出了对该事件序列的控制权,这也就是外部拦截法能实现的原因。
内部拦截法
继续看看内部拦截法:
//父view.java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } //子view.java @Override public boolean dispatchTouchEvent(MotionEvent event) { //父view拦截条件 boolean parentCanIntercept; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(event); }
内部拦截法是将主动权交给子View,如果子View需要事件就直接消耗,否则交给父容器处理。我们列举下DOWN和MOVE两种情况:
ACTION_DOWN
的时候,子View必须能消费,所以父View的onInterceptTouchEvent
要返回false,否则就被父View拦截了,而且后续事件也不会传到子View这里了。ACTION_MOVE
的时候,父View的onInterceptTouchEvent
方法要返回true,表示当子View不想消费的时候,父View能及时消费,那么子View怎么控制呢?可以看到代码设置了一个requestDisallowInterceptTouchEvent
方法,这个是干嘛呢?
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000; @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } }
这种通过|=
和 &= ~
运算符修改参数是源码中常见的设置标识的方法:
|=
将标志位设置为1&= ~
将标识位设置为0
所以在需要父元素拦截的时候就设置了requestDisallowInterceptTouchEvent(false)
方法,让标志位设置为0,这样父元素就能执行到onInterceptTouchEvent方法。
具体生效代码就在dispatchTouchEvent
方法中:
if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } }
可以看到,如果disallowIntercept
为false,就代表父View要拦截,然后就会执行到onInterceptTouchEvent
方法,在onInterceptTouchEvent
方法中返回ture,父View成功拦截。
总结
经过拇指记者的探访,终于把Android公司对于事件任务处理摸清楚了,希望对于屏幕前的你能有些帮助,下期再见啦。
参考
《Android开发艺术探索》
https://wanandroid.com/wenda/show/12119
http://gityuan.com/2016/12/31/input-ipc/
https://juejin.cn/post/6844903926446161927
https://www.jianshu.com/p/b7f33f46d33c
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1868