Android触摸事件-02ViewGroup触摸事件及源码分析

参考文档

API

  • ViewGroup的触摸事件处理,很多继承于view,一方面它重载了dispatchTouchEvent,另外一个主要的区别在于新添加了函数onInterceptTouchEvent

dispatchTouchEvent

  • 如果ViewGroup的某个孩子没有接受ACTION_DOWN事件;那么,ACTION_MOVE和ACTION_UP等事件也一定不会分发给这个孩子
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    // 调试用
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    // 第1步:根据遮挡情况,判断是否需要分发该触摸事件
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 第2步:检测是否需要清空目标和状态
        //
        // 如果是ACTION_DOWN,说明是一个新的触摸事件序列,则清空之前的触摸事件处理target和状态。
        // 这里的情况状态包括:
        // (01) 清空mFirstTouchTarget链表,并设置mFirstTouchTarget为null。
        //      mFirstTouchTarget是"接受触摸事件的View"所组成的单链表
        // (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT标记
        //      如果设置了FLAG_DISALLOW_INTERCEPT,则不允许ViewGroup对触摸事件进行拦截。
        // (03) 清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN标记
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }    

        // 第3步:检查当前ViewGroup是否想要拦截触摸事件
        //
        // 如果是ACTION_DOWN,也就是一个事件序列的开始,当然要重新判断当前ViewGroup是否拦截了事件
        // 而如果不是ACTION_DOWN事件,并且mFirstTouchTarget为null的话,也就是说前面的ACTION_DOWN事件不是由子view去处理的,
        // 因而以后的ACTION_UP,ACTION_MOVE等事件也不会交由子view去处理,所以就相当于直接拦截了事件,直接返回了true
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 检查禁止拦截标记:FLAG_DISALLOW_INTERCEPT
            // 如果调用了requestDisallowInterceptTouchEvent()标记的话,则FLAG_DISALLOW_INTERCEPT会为true,不允许拦截
            // 例如,ViewPager在处理scroll事件的时候,就会调用requestDisallowInterceptTouchEvent(),
            // 禁止它的父类对触摸事件进行拦截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 允许自身拦截的话,返回onInterceptTouchEvent()
                intercepted = onInterceptTouchEvent(ev);
                // restore action in case it was changed
                ev.setAction(action);
            } else {
                //不允许拦截的话,当然也就没有拦截
                intercepted = false;
            }    
        } else {
            //up, move事件,并且没有子view可以处理down,当然也没有子view处理up,move,直接相当于拦截掉了
            intercepted = true;
        }    

        // 第4步:检查当前的触摸事件是否被取消
        //
        // (01) 对于ACTION_DOWN而言,mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVENT位肯定是0;因此,canceled=false。
        // (02) 当前的View或ViewGroup要被从父View中detach时,PFLAG_CANCEL_NEXT_UP_EVENT就会被设为true;
        //      此时,它就不再接受触摸事情。
        final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;

        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

        // 第5步:将触摸事件分发给"当前ViewGroup的子View和子ViewGroup"
        //
        // 如果触摸"没有被取消",同时也"没有被拦截"的话,则将触摸事件分发给它的子View和子ViewGroup。
        // 整一个if(!canceled && !intercepted){ … }代码块所做的工作就是对ACTION_DOWN事件的特殊处理。
        // 因为ACTION_DOWN事件是一个事件序列的开始,所以我们要先找到能够处理这个事件序列的一个子View,
        // 如果一个子View能够消耗事件,那么mFirstTouchTarget会指向子View,
        // 如果所有的子View都不能消耗事件,那么mFirstTouchTarget将为null
        if (!canceled && !intercepted) {
            //MotionEvent.ACTION_HOVER_MOVE一般指鼠标事件,鼠标在view上面
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 这是获取触摸事件的序号 以及 触摸事件的id信息。
                // 对于down事件,一般是0
                final int actionIndex = ev.getActionIndex();
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // 清空这个手指之前的TouchTarget链表。
                // 一个TouchTarget,相当于一个可以被触摸的对象;它中记录了接受触摸事件的View
                removePointersFromTouchTargets(idBitsToAssign);

                // 获取该ViewGroup包含的View和ViewGroup的数目,
                // 然后递归遍历ViewGroup的孩子,对触摸事件进行分发。
                // 递归遍历ViewGroup的孩子:是指对于当前ViewGroup的所有孩子,都会逐个遍历,并分发触摸事件;
                //   对于逐个遍历到的每一个孩子,若该孩子是ViewGroup类型的话,则会递归到调用该孩子的孩子,...
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    final View[] children = mChildren;

                    final boolean customOrder = isChildrenDrawingOrderEnabled();
                    //倒序遍历,一般都是希望最上面的反馈
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder ?
                                getChildDrawingOrder(childrenCount, i) : i;
                        final View child = children[childIndex];
                        // 如果child不能够接受触摸事件,又或者触摸坐标(x,y)在child的可视范围之外
                        // 就继续循环查找可以分发事件的子View
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }

                        // getTouchTarget()的作用是查找child是否存在于mFirstTouchTarget的单链表中。
                        // 是的话,则更新相应的值
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        // 重置child的mPrivateFlags变量中的PFLAG_CANCEL_NEXT_UP_EVENT位。
                        resetCancelNextUpFlag(child);

                        // 将事件尝试分发给子View,如果找到了一个可以处理触摸事件的子View,就直接跳出循环,不用继续遍历
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = childIndex;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 如果child能够接受该触摸事件,即child消费或者拦截了该触摸事件的话;
                            // 则调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头,并返回表头对应的TouchTarget
                            // 同时还设置alreadyDispatchedToNewTouchTarget为true。
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }

                // 如果newTouchTarget为null,并且mFirstTouchTarget不为null;
                // 则设置newTouchTarget为mFirstTouchTarget链表中第一个不为空的节点。
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 第6步:进一步的对触摸事件进行分发,实际上是对拦截的了,取消了的,或其它非ACTION_DOWN事件的处理
        //

        if (mFirstTouchTarget == null) {
            // 如果mFirstTouchTarget为null,意味着还没有任何View来接受该触摸事件;
            // 此时,将当前ViewGroup看作一个View;
            // 将会调用"当前的ViewGroup的父类View的dispatchTouchEvent()"对触摸事件进行分发处理。
            // 注意:这里的第3个参数是null,也就是直接将事件交由这个ViewGroup处理
            // 即,会将触摸事件交给当前ViewGroup的onTouch(), onTouchEvent()进行处理。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
            // 如果mFirstTouchTarget不为null,说明ACTION_DOWN事件已经有子View处理了,
            // 那么对于其它类型的事件,直接尝试分发给mFirstTouchTarget链表中的就可以了
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 这里实际上区分开了ACTION_DOWN与其它类型的事件
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 第7步:再次检查取消标记,并进行相应的处理
        //
        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    // 调试用
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

onInterceptTouchEvent

  • onInterceptTouchEvent是否拦截触摸事件,默认情况下是不拦截的

  • 常见的例子就是ViewPager,当发生滑动事件的时候,ViewPager拦截了事件,用来左右滑动item

    public boolean onInterceptTouchEvent(MotionEvent ev) {
      return false;
    }
    
  • 当ViewGroup拦截了down事件,或者没有子view去处理down事件,后续的up,move事件就不会调用 onInterceptTouchEvent()方法了,所以该方法并不是每次事件都会调用的

图解

viewgroup.png

总结

  • ACTION_DOWN事件为一个事件序列的开始,中间有若干个ACTION_MOVE,最后以ACTION_UP结束。
  • ViewGroup默认不拦截任何事件,所以事件能正常分发到子View处(如果子View符合条件的话),如果没有合适的子View或者子View不消耗ACTION_DOWN事件,那么接着事件会交由ViewGroup处理,并且同一事件序列之后的事件不会再分发给子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的话,那么最后事件会交由Activity处理。即:逐层分发事件下去,如果都没有处理事件的View,那么事件会逐层向上返回。
  • 如果某一个View拦截了事件,那么同一个事件序列的其他所有事件都会交由这个View处理,此时不再调用View(ViewGroup)的onIntercept()方法去询问是否要拦截了
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容