浅谈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 确定几条规则

  1. 通过 ctrl+f 查找发现,在dispatchTouchEvent 方法中间没有return的情况,只会在最后return handled。那么我们的目的就是观察handled在整个方法中的变化。

  2. handled在本方法中总共被赋值了四次,其中初始的时候赋值为false,其余三次皆在2.4内,所以但看handled的赋值情况,还不是特别费劲。

  3. 记住几个重要的变量:mFirstTouchTargetdisallowInterceptintercepted

    • mFirstTouchTarget是一个TouchTarget,里面存有有可能处理点击事件的子View,TouchTarget是一个单链表元素,而且在本文中的使用都是前序插入。mFirstTargets是单链表的表头
    • disallowIntercept,boolean量,顾名思义,disallow to intercept event,是否不允许拦截事件。读起来很拗口,用起来一般都是双重否定表示肯定,!disallowintercept,是否允许拦截。
    • intercepted,boolean量,在当前的view(viewgroup)是否拦截了此事件。
  4. Last but not least,网上盛传的三大方法dispatchTouchEventonInterceptTouchEventonTouchEvent以及那段神奇的伪码(不知道也没关系,下一节会分析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与或的问题,推荐一篇文章:

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;
            }

        }
    }
}

具体的细节是真的说不完,我们只说一般情况吧。

  1. 我们获得所有的子View,逆序遍历,也就是从前到后遍历,把符合要求的子View挑出来。符合啥要求呢?

    • 点击事件是否发生在此view的范围内
    • view是否发生着动画效果
  2. 还存在特殊情况,就是如果你遍历着遍历着,发现一个View已经存在在targets链表中,那么直接终止循环。

  3. 这里注意一下,调用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.
    
}
  1. 如果targets链表是空的,说明所有的子view都不中用啊,给它们机会他们不用,那只能让本viewgroup执行

  2. 如果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;
}

  1. 我们看看handled的赋值情况

    • 如果mFirstTouchTargets为空,则handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    • 如果不为空
      • 如果上个模块里alreadyDispatchedToNewTouchTarget = true;,那么就憋往下进行了,handled = true
      • 否则则对剩下的targets链表里的target进行遍历,通过dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法来调用他们的dispatchTouchEvent方法,将返回值赋值给handled。
  2. 一直说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的事件分发正在分析中。。。。。。