Android事件分发机制之ViewGroup

一,写在前面  

       相信大家一定遇到过这样的问题:在滑动某一控件时,由于有多个控件都能处理当前滑动,比如:Viewpager中有一个子元素是Viewpager,那么在左右滑动的时候,到底是哪个Viewpager控件去处理事件,并最终消费掉事件呢?这里其实就是产生了滑动冲突了,就像游戏里打野抢ad资源,本来团队是将资源给ad发育的,打野一个技能收掉了,于是需要一个服众的领导者去处理冲突,让该拿资源的人吃资源。而,事件滑动冲突发生了,作为领导者的我们,就去处理冲突,于是要理解冲突发生的原因,这样控件才能“心服口服”。

二,初识事件分发

         当我们的手触摸到屏幕上时,一般有这样三个操作Action_down,Action_move,Action_up,分别对应手指的按下,移动,擡起操作。该三个操作加在一起是一个事件序列,按照Action_down,Action_move,Action_up的顺序。当有一个事件发生时,最开始接受该事件的是应用程序的窗口-Activity,然后交给PhoneWindow处理,再传递给DecorView处理,最后会传递给setContentView(R.layout.xxx)的根View,然后传递给下一层的子元素(若子元素有多个,则会按时间逆序传递,即,最晚添加的子元素,最先传递),一层一层类此......直到传递到最后的叶子节点(必为非容器控件)。

        在前面,一层层传递的过程中,是假设传递过程中没有控件拦截事件,这样才能传递到最后的叶子节点。当事件传递到某一个view时,这个view的dispatchTouchEvent(ev)会被调用,若该view对该事件拦截,那么onInterceptTouchEvent(ev)被调用并返回true,并执行onTouchEvent(ev)对事件进行具体的处理。完整的事件处理走向,这里就不码文字介绍了,后面会通过源码来解释事件分发的流程。

        方法介绍

         下面介绍三个方法:

         dispatchTouchEvent(ev):只要事件传递到View,就会调用该方法。返回true,则事件被处理并消费;返回false,则事件没有消费掉,如果是action_down,事件交给父view处理;如果是action_move,action_up,则事件直接交给Activity处理。

         onInterceptTouchEvent(ev):该方法只有继承了ViewGroup的容器控件才有,原始的View(非容器控件)没有子View,因此没有该方法。该方法被调用后,如果返回true,代表要拦截事件,则view会调用onTouchEvent(ev)处理事件;如果返回false,代表不拦截,则事件会传递到子View。

         onTouchEvent(ev):容器控件和原始的View处理事件的地方,如果返回true,则事件被消费,dispatchTouchEvent(ev)返回true;如果返回false,则事件没有消费。

下面列出Activity,View,Viewgroup,AbsListView,TextView的关系图,以便清晰认识哪些方法是继承的,哪些方法是重写。如下:


下面展示上述三个方法在处理事件时的伪代码:

		public boolean dispatchTouchEvent(MotionEvent ev) {
			//是否消费标志
			boolean isConsume = false;
			if (onInterceptTouchEvent(ev)) {
				//如果拦截
				isConsume = onTouchEvent(ev);
			} else {
				//子元素处理
				isConsume = child.dispatchTouchEvent(ev);
			}
			return isConsume;
		}

三,源码分析

事件最开始交给Activity处理,查看Activity$dispatchTouchEvent源码:

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * @param ev The touch screen event.
     * 
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }


        参数MotionEvent:对事件的触摸位置,时间等数据的封装。事件的传递具体在代码上的体现,就是对MotionEvent对象的传递处理。

        当getWindow().superDispatchTouchEvent(ev)为true时,return true,dispatchTouchEvent对事件处理完毕,事件交给if条件里对应的view处理了(具体是哪个view后面会分析)。如果getWindow().superDispatchTouchEvent(ev)返回false,即没有view处理,会执行return onTouchEvent(ev),交给Activity的onTouchEvent(ev)处理,不再说明Activity的onTouchEvent(ev)如何处理事件,这不是本篇文章重点。
        接下来看if 条件里的内容,getWindow()对应Window的唯一子类PhoneWindow,查看PhoneWindow$superDispatchTouchEvent(ev)源码:

 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor就是DecorView对象,事件于是交给DecorView的superDispatchTouchEvent(ev)处理,查看该源码:

 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

	//...code
	public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
	//...code
 }
        该方法里调用了super.dispatchTouchEvent(event)对事件进行处理,分析DecorView的继承关系,它是继承FrameLayout,说明事件转发肯定会调用View里的dispatchTouchEvent(ev)。然后事件会传递到setContentView(R.layout.xxx)对应布局文件里的根View,然后一层层转发。至于,事件是如何从DecorView传递到根View的,不作分析,可以肯定的是事件肯定是传递到了根View,不然如何能响应点击事件呢,这里不影响分析事件分发,主要是研究根View以后的转发情况,来理解处理滑动冲突。


       一般情况,根View是一个容器控件,然后向下传递事件,那么就先拉出容器控件的爸爸->ViewGroup的代码进行分析。查看ViewGroup$dispatchTouchEvent(ev)源码如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //  ...code

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

//A
		
            // 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);
		//设置变量mGroupFlags的最高位为0
                resetTouchState();
            }
//B
            // Check for interception.
            final boolean intercepted;
	    //若子View处理并消费了action_down事件,mFirstTouchTarget不为空
            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;
            }
//C
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        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];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }

                    // code...
                }
            }
//D
            // 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.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    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;
                }
            }

            //code ...
        return handled;
    }



    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
        注意:在上面代码中,注释有ABCD4个区域,分4块来分析,一块块解决。现在先以ACTION_DOWN事件为例来分析源码,然后再分析action_move,action_up事件。下面类似于A-13这样的标号:代表A区域,代码的第13行。


        现在有一个action_down事件传递到了容器控件(缩写为vp),于是dispatchTouchEvent(ev)被调用。第5行定义的 boolean handled = false;会被return,它的值表明该事件是否被消费掉了。执行到A-13,进入if语句,会调用resetTouchState(),进入该方法发现有这样一段代码:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;目的是设置变量mGroupFlags的最高位为0,其中FLAG_DISALLOW_INTERCEPT为0x8000;也就是说只要是action_down事件传递过来,那么会对字段mGroupFlags进行一个最高位为0的处理(后面自有用处)。

        程序继续指定到A-25行,由于这里是action_down事件,那么actionMasked == MotionEvent.ACTION_DOWN为true,变量mFirstTouchTarget是指当vp的子元素处理消费了action_down/move/up事件时,mFirstTouchTarget指向一个对象地址,不为空(后面自有用处)。

       

        B-25值为true,进入if语句,分析B-27行:mGroupFlags值受上面提到resetTouchState()的改变,还会受ViewGroup$requestDisallowInterceptTouchEvent(boolean)的改变,该方法会使mGroupFlags的最高位为1。action_down事件在设置了mGroupFlags最高位为0,会使disallowIntercept值为false,进入B-28的if语句中,执行intercepted = onInterceptTouchEvent(ev)。requestDisallowInterceptTouchEvent一般在子View中调用,使mGroupFlags的最高位为1,会使disallowIntercept值为true,进入B-31的else语句中,不让vp执行onInterceptTouchEvent(ev),而直接设置intercepted = false,不让父控件vp拦截该事件。注意:若事件为action_down,子控件调用requestDisallowInterceptTouchEvent方法无法阻止父控件vp对事件进行拦截,原因:前面A-13的作用就是为了让父控件始终能有机会拦截到action_down事件。至于,最终vp是否拦截事件,还要看onInterceptTouchEvent方法里面的具体处理。于是,查看ViewGroup$onInterceptTouchEvent(ev)源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
         哇,ViewGroup$onInterceptTouchEvent(ev)始终返回false,不拦截事件。所以一般容器控件了为了处理一些滑动,会重写onInterceptTouchEvent(ev)对事件进行拦截,例如AbsListView。

   

        那么B-32行的else语句什么时候执行呢?第一种情况:若前面的action_down/move/up事件被vp拦截了(onInterceptTouchEvent(ev)返回true),那么intercepted = true,不会再执行onInterceptTouchEvent(ev)方法,vp会将一个事件序列全部拦截处理;第二种情况:若vp没有拦截事件action_down/move/up,但是子view处理事件后,没有消费掉事件,会使mFirstTouchTarget为null,执行intercepted = true,后面的事件交给父控件vp处理了。

         结论

        1,事件为action_down时,容器控件Viewgroup肯定会执行onInterceptTouchEvent(ev)方法,判断是否要拦截,;若拦截了,则后面传递的action_move/up事件不会再执行onInterceptTouchEvent(ev)方法判断是否拦截,而是都拦截给vp自己处理,这里就没有子View什么事啦。

        2,若action_down事件能传递到子View,即vp没有拦截,若子View及其子元素没有一个view能消费掉action_down事件,那么该事件会交给子View的父控件vp处理(后面有代码分析证明),同时一个序列事件中的action_move/up也不会再传递给子View处理,而是都交给它的父View处理。(上面一段已证明)

3,在2的基础上,子View及其子元素有一个view能消费掉action_down事件时,但后面的action_move/up事件没有消费,那么该事件不会再给父控件vp处理,而是直接交给Activity处理。(后面有分析证明这点,这里先一起提出来)

         

        继续分析源码(这里还是先分析action_down):如果vp不拦截action_down事件,那么C-48中值为true,进入if语句;C-49,C-50,C-51都是判断action_down事件的,其他事件无法进入if语句。执行到C-61,若vp的子元素不为0,则进入if语句;继续执行到C-69,遍历子元素,执行到C-73,C-74。isTransformedTouchPointInView(x, y, child, null):代表触摸点是否在该子元素布局内,canViewReceivePointerEvents(child):是否在执行动画。若两者均为true,则继续执行到C-87;否则执行continue,当前循环结束,遍历下一个子元素。

         代码执行到C-87,调用dispatchTransformedTouchEvent方法,这里就是子View处理action_down的地方,查看ViewGroup$dispatchTransformedTouchEvent源码,里面有这样一段代码:

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
         简单解释下:当child为null时,事件由vp处理,并调用super.dispatchTouchEvent(event)处理。(后面会分析这个调用)

        child不为空时,上面C-87的child就不为空,调用child.dispatchTouchEvent(event),这就是前面说的事件传递到view,那么会调用view的dispatchTouchEvent(event)方法。若child为原始的View(非容器控件),无法再传递事件了,那么直接交给child处理。若child为容器控件,则继续重复上面的流程,将事件依次转发下去,直到有一个view消费了事件,child.dispatchTouchEvent(event)返回true,handled为true;如果没有一个view消费事件,那么child.dispatchTouchEvent(event)返回false,handled为false。

        这里的handled对应C-87的if语句的条件,若条件为true,会执行newTouchTarget = addTouchTarget(child, idBitsToAssign),alreadyDispatchedToNewTouchTarget = true。若条件为false,则不执行上面两步。进入ViewGroup$addTouchTarget源码:

/**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
        addTouchTarget方法内部会对mFirstTouchTarget设置值,使其不为空。于是证明前面的分析:当子View消费了action_down/move/up事件时,mFirstTouchTarget不为空。另外,还设置标志位alreadyDispatchedToNewTouchTarget 为true。(后面自有用处)


       继续分析源码,现在分析到了D区域,这里将对ViewGroup$dispatchTouchEvent(ev)方法确定返回值,反应事件在容器控件vp的处理情况。如果返回值handled为true,则该事件(action_down/move/up)被消费了,会使得Activity$dispatchTouchEvent(ev)中的getWindow().superDispatchTouchEvent(ev)) 的值为true,执行if语句,return true,消费了事件,事件处理结束;反之,继续执行Activity$dispatchTouchEvent(ev)中的return onTouchEvent(ev);将事件交给Activity处理。

       流程执行到D-105,若action_down事件被vp拦截,或者能传递到子View但是没消费。那么,if语句值为true,执行handled = dispatchTransformedTouchEvent(ev, canceled, null,ouchTarget.ALL_POINTER_IDS),参数里的null,就是前面说的child为null的情况,执行super.dispatchTouchEvent(event)。这里ViewGroup的super就是View啦,所以容器控件vp处理事件是调用View$dispatchTouchEvent(ev),里面调用onTouchEvent(ev)处理事件,下篇文章会分析View$dispatchTouchEvent(ev)如何决定是否消费事件,这里就打住了。事件处理最终会交给onTouchEvent方法(下一篇文章会讲),若onTouchEvent(ev)返回true,则super.dispatchTouchEvent(ev)返回true,消费了事件,那么此事件的处理就结束了;反之,交给Activity处理。执行了D-105的if语句内容,那么else中不会执行。


        若子view消费了action_down事件,D-116行if条件表达式值为true,action_down事件消费了,该事件处理流程结束。若子view已经消费了action_down事件,现在传递的是action_move/up,则代码会执行到D-121,D-122行,如果能消费action_move/up事件,return true,该事件处理流程结束;若不能消费,即D-121,D-122行条件表达式为false,那么handled不设置为true,return默认的值false,事件不会交给父View处理,直接交给Activity处理。(此处证明前面的一个结论)

四,另外

       最后,分析到这里,相信不管是action_down事件,还是其他action_move/up事件,这些事件在View中的分发流程基本理清楚了。但是,还没有介绍事件交给View$onTouchEvent(ev)处理的流程,放在下一篇博客中分析吧。本篇文章基于源码的分析,并没有列出利用其解决滑动冲突的方案,但是相信理清了事件分发的流程,稍微记住些小结论用于实践,处理滑动冲突就可以知其所以然的。其实滑动冲突有两种解决方案:1,外部拦截法;2,内部拦截法。这里先作一个引子,后面会码一篇blog展示如何处理滑动冲突。


       





            

        





发布了47 篇原创文章 · 获赞 28 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章