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

  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的事件分发正在分析中。。。。。。

全部评论

相关推荐

身边的人都在收获,我却还在原地踏步,到底该怎么办啊!每次看到他们的好消息,我都想放弃,心里不停地问自己:到底该怎么才能找到一份工作呢?这种无力感让我想要彻底摆烂,真的很想知道,别人是怎么做到的。有没有人分享一下经历呢?我想学习一下啊走出这样的日子。
鼗:秋招其实是运气>实力的一场竞技游戏,除非实力很强(学历和技术)。大多数人都是半斤八两,看面试官和HR以及简历被曝光的概率罢了,有些时候你可能运气差一点或者说面试官不太友好也或者说你确实准备的不够好之类的,这些都是可能发生的事情。我觉得能做的事情是不比较、不气馁、在面试前多看一点面试的时间冷静一点自信一点,大大方方面试,给自己多一点时间去求职。我这样说不是站着说话不腰疼,我是想说你的offer还在路上,你也值得在这些困难之后得到你较为理想的offer,请你继续加油,保持乐观,积极打败你现在的困难
点赞 评论 收藏
分享
09-27 00:29
东北大学 Java
伟大的麻辣烫:查看图片
阿里巴巴稳定性 74人发布 投递阿里巴巴等公司10个岗位
点赞 评论 收藏
分享
过往烟沉:我说什么来着,java就业面就是广!
点赞 评论 收藏
分享
11-05 18:26
门头沟学院 Java
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务