浅谈Android事件分发——ViewGroup里的事件分发
文章目录
浅谈ViewGroup里的事件分发
为啥一上来就是标号2?因为我太懒了,目标是分析Activity、ViewGroup以及View里面的事件分发,但是今天才有勇气看源码,只看了ViewGroup的。用一天没上网课的代价记录了一下看源码的理解,毕竟只有一天,一定会有很多不足和理解错误,希望能够抛砖引玉。
2.ViewGroup内的事件分发
dispatchTouchEvent方法的代码(别害怕,没有大批源码,都是抽出来的方法)
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
---------------------------------------------------------------
---------------------------------------------------------------
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
}
---------------------------------------------------------------
---------------------------------------------------------------
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
} else {
}
---------------------------------------------------------------
---------------------------------------------------------------
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
---------------------------------------------------------------
---------------------------------------------------------------
// Update list of touch targets for pointer down, if needed.
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
}
}
---------------------------------------------------------------
---------------------------------------------------------------
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
}
---------------------------------------------------------------
---------------------------------------------------------------
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
}
---------------------------------------------------------------
---------------------------------------------------------------
}
if (!handled && mInputEventConsistencyVerifier != null) {
}
return handled;
2.1 确定几条规则
-
通过
ctrl+f
查找发现,在dispatchTouchEvent 方法中间没有return的情况,只会在最后returnhandled
。那么我们的目的就是观察handled在整个方法中的变化。 -
handled在本方法中总共被赋值了四次,其中初始的时候赋值为
false
,其余三次皆在2.4
内,所以但看handled的赋值情况,还不是特别费劲。 -
记住几个重要的变量:
mFirstTouchTarget
、disallowIntercept
、intercepted
- mFirstTouchTarget是一个TouchTarget,里面存有有可能处理点击事件的子View,TouchTarget是一个单链表元素,而且在本文中的使用都是前序插入。mFirstTargets是单链表的表头
- disallowIntercept,boolean量,顾名思义,disallow to intercept event,是否不允许拦截事件。读起来很拗口,用起来一般都是双重否定表示肯定,
!disallowintercept
,是否允许拦截。 - intercepted,boolean量,在当前的view(viewgroup)是否拦截了此事件。
-
Last but not least,网上盛传的三大方法
dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
以及那段神奇的伪码(不知道也没关系,下一节会分析view的时候会用到的)是针对View来说的,分析ViewGroup的时候先别在脑海中形成定势思维,等到我们说到View的时候自然会用到,现在先不用想。
2.2 Handle initial down
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
2.1 里我们提到了mFirstTouchTarget,它相当于一个链表的头结点,这个链表将当前ViewGroup中的子View里符合分发事件的条件的都构造成TouchTarget,并组成一个前序插入的链表,mFirstTouchTarget就是头结点。
cancelAndClearTouchTargets(ev)
以及resetTouchState()
都会调用clearTouchTargets()
,而clearTouchTargets()
的作用如下:
由于一个事件从起始到结束一般都是以ACTION_DOWN
开头,经过很多的ACTION_MOVE
以后,以ACTION_UP
结束,所以我们监测Event是否为ACTION_DOWN
,从而判断是否是一个新的事件组。当发现是ACTION_DOWN
时,我们知道,一组新的事件开始了,那么前朝的忠臣mFirstTouchTarget就要换代了,得先把他的位置空出来,置空,没了链头,整个链表也就没了,所以这也解释了为什么要用前序插入的方法。
resetTouchState()
还有一个作用就是重置mGroupFlags
的值,这个值会影响到disallowIntercept
。
关于Android中Flag与或的问题,推荐一篇文章:
2.2 Check for interception
经过了初始化后,我们就要判断是当前的ViewGroup是否要拦截该事件。
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
} else {
}
判断是否拦截就有两种分支了
// Check for interception.
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;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
- 如果是ACTION_DOWN事件或者子View可以拦截事件
-
如果
disallowIntercept
为true,这时用到上面的一个关键量disallowIntercept
,给它赋值?final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
这个你就直接认为它为false就行了(在没有其他操作的情况下)。为啥要加这个量?因为我们可以通过在子View操控FLAG_DISALLOW_INTERCEPT
进而控制disallowIntercept
,这个量你也可以理解为我子View是否同意你去拦截,不管你接下来是否拦截,我不管,只要disallowIntercept
为true,我就撒泼打滚,就是不同意你继续操作了,不让你判断了,你intercepted直接就为false就行了,不要拦截我的事件。但是要注意,这种不同意只能针对除ACTION_DOWN以外的事件,因为每次ACTION_DOWN都会先调用2.2里的方***把你子View设置的FLAG_DISALLOW_INTERCEPT
给换掉,换成0x80000,直接把你的撒泼打滚抹杀掉了。 -
如果disallowIntercept为false,即同意我继续判断,我就可以调用
onIntercepterTouchEvent(ev)
,还是一样,在没有重写的时候,ViewGroup的这个方法默认返回false。
-
- 如果既不是ACTION_DOWN也没有子View可以拦截事件,则
intercepted
设置为false.
经过上面的操作后,正常情况下,intercepted
会为false。也就是本ViewGroup不对事件进行拦截,那么我们就可以进行下一个操作——找它孩子里所有有能力或者有资格拦截事件的。
2.3 Update pointer down touch targets list
可以看下面的代码,canceled别管,一般都是false,那么 intercepted
这时候就派上用场了。如果我们ViewGroup本身不拦截,那么就要从子View中寻找能够拦截的事件并且构造一个链表。
// Update list of touch targets for pointer down, if needed.
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//循环逆序遍历所有的子view
for() {
final View child = getAndVerifyPreorderedView(xxxx);
if (点击事件不在View的范围内 || view正在执行动画) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget不为空) {
//判断条件翻译一下就是:
//newTouchTarget在FirstTouchTargets为头的链表内
break;
}
//这个判断条件翻译一下就是如果这个child,也就是子view
//想处理点击事件,它就返回true
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//记住这个量哦,一定要记住。
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
具体的细节是真的说不完,我们只说一般情况吧。
-
我们获得所有的子View,逆序遍历,也就是从前到后遍历,把符合要求的子View挑出来。符合啥要求呢?
- 点击事件是否发生在此view的范围内
- view是否发生着动画效果
-
还存在特殊情况,就是如果你遍历着遍历着,发现一个View已经存在在targets链表中,那么直接终止循环。
-
这里注意一下,调用
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
方法其实最终就是调用了这个子view也就是child的dispathchTouchEvent方法,如果返回true,就说明这个child可以处理这个事件,就把这个child添加到targets链表中去,并且记录alreadyDispatchedToNewTouchTarget = true;
,说明已经有一个view可以处理事件了。记住这个量,接下来有用😀,然后循环结束。
2.4 Dispatch to targets
事件分发
现在就到了我们最终的目标——事件分发了。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
}
-
如果targets链表是空的,说明所有的子view都不中用啊,给它们机会他们不用,那只能让本viewgroup执行
-
如果targets不为空,则让子view去执行,但是要注意,是从链表的第二个开始的,因为第一个target已经在上一个步骤被执行过了。
虽然我很不想抄代码,但是还是要抄一次,我们一起看一下这分发到else里的情况
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;
}
-
我们看看handled的赋值情况
- 如果mFirstTouchTargets为空,则
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
- 如果不为空
- 如果上个模块里
alreadyDispatchedToNewTouchTarget = true;
,那么就憋往下进行了,handled = true
- 否则则对剩下的targets链表里的target进行遍历,通过
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
方法来调用他们的dispatchTouchEvent
方法,将返回值赋值给handled。
- 如果上个模块里
- 如果mFirstTouchTargets为空,则
-
一直说
dispatchTransformedTouchEvent
,这里面干嘛了?
来来来,咱们看看里面关键的代码
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
当你看到supr.dispatchTouchEvent时,就发现了,事件开始流入了View里的dispathcTouchEvent方法了。
2.5 Update point up or cancel tarch targets list
不占主要位置,先略过了。
Activity以及View的事件分发正在分析中。。。。。。