Android进阶6:View的事件分发机制源码

前言

关于事件分发机制,这个东西对于开发者,很重要,例如:解决滑动冲突;对于面试者,也很重要,比如:请描述一下View的事件分发机制~。说句实话,这个源码是看了很长时间,一直不敢写这篇文章,生怕误人子弟啊,说实话,刚开始刚觉很难,但是硬着头皮再看,也就那么回事了。。。

View分发机制的相关方法

View

  • dispatchTouchEvent(ev) : 专门处理事件
  • onTouchEvent(ev) :

ViewGroup

  • dispatchTouchEvent(ev) : 它做的也就两件事,首先判断是否拦截事件;如果不拦截的话,就是往下分发事件给子View。
  • onInterceptTouchEvent(ev) : 是否拦截该事件,如果true,交给自己的onTouchEvent处理;false,就表示不拦截该事件
  • onTouchEvent(ev) : ViewGroup内部没有重写该方法;

注意:这里的事件指的是:从手指触碰ViewGroup到离开ViewGroup的一系列事件。

View分发流程

假如此时有两个ViewGroup_1, ViewGroup_2以及一个View,其中 ViewGroup_1包含ViewGroup_2, ViewGroup_2包含View,那么此时默认情况下事件分发的流程就是:

ViewGroup_1——dispatchTouchEvent
ViewGroup_1——onInterceptTouchEvent

ViewGroup_2——dispatchTouchEvent
ViewGroup_2——onInterceptTouchEvent

View——dispatchTouchEvent
View——onTouchEvent

ViewGroup_2——onTouchEvent
ViewGroup_1——onTouchEvent

上个图:在这里插入图片描述

解释一波上图流程:
1:默认情况下,先调用ViewGroup的dispatchTouchEvent()方法 ,如果返回true,就不再往下分发,如果是默认值(false),那么调用onInterceptTouchEvent();
2:如果onInterceptTouchEvent()方法返回true,代表拦截事件,那么就交给自己的onTouchEvent()处理事件;如果返回默认值,那么就表示不拦截事件,继续向下分发,将事件交给子ViewGroup或者子View;
3:如果child是ViewGroup,那么递归1,2步骤。 如果child是View, 那么就先调用View 的dispatchTouchEvent事件,如果不消耗事件,那么调用其onTouchEvent事件,如果不消耗事件,那么调用父ViewGroup的onTouchEvent事件。换句话说:隧道式分发,冒泡式消费!

其实看到这里,建议各位还是先明白每个方法返回什么值,会产生什么样的效果,这样接下来看源码会舒服很多。

ViewGroup源码分析

我们点击的Activity,首先进入到Activity的dispatchTouchEvent源码:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

可以看到首先接收到ACTION_DOWN的时候,首先调用的是自己的onUserInteraction方法,onUserInteraction方法是个空的实现,用户可以重写该方法,处理事件开始分发的相关逻辑,总体来说就是告诉开发者:我要开始事件分发了啊!
接下来调用了window的superDispatchTouchEvent方法,如果该方法返回false(不消耗事件),那么就调用自己的onTouchEvent方法;好,接下来看下getWindow().superDispatchTouchEvent(ev),那么window是什么时候初始化的时候呢?
是在Activity中的attach方法中初始化的:

 final void attach(Context context, ActivityThread aThread,
	 .......
         //创建PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ........
 }

好,到这里我们知道了getWindow().superDispatchTouchEvent(ev)实际上调用的是PhoneWindow的方法,另外可以看下Window的注释,注释说的很清楚:PhoneWindow是Window的唯一实现。

进入到PhoneWindow的superDispatchTouchEvent方法:

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

这里的mDecor也就是DecorView对象,而DecorView也是继承自FramLayout,里面装的是我们显示的布局,也就是setContentView(View),在进入到DecorView的superDispatchTouchEvent方法:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

调用了父类的dispatchTouchEvent方法,所以从触摸Activity开始路径为:activity——phonWindow——DecorView——ViewGroup——我们自己的布局。
终究调用的是ViewGroup的dispatchTouchEvent(ev)方法,进入到源码:

            // 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();
            }

上述代码首先就是清空标记,也就是当每一次是down手势的时候,就清空标记,清空什么标记呢?
1:mFirstTouchTarget值null

    /**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

2:重置FLAG_DISALLOW_INTERCEPT标记 (mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;)。
至于这两个标记是什么下文看。

    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

那么根据我们对事件分发结果观察,ViewGroup执行dispatchTouchEvent(ev)之后就是执行了onInterceptTouchEvent(ev)方法,当然了,条件是默认情况下。
根据上述结果,看源码:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {       
                    //是否不允许拦截
                    //disalllowIntercept的值可以通过requestDisallowInterceptTouchEvent设置,默认值是false,
                    //如果是true,!disallowIntercept = false, 此时:走else  intercepted = false;,也就是不拦截事件,不执行onInterceptTouchEvent方法
                    //默认是false,此时!disallowIntercept = true,也就是说:子View可以让父View拦截事件。此时走if, 执行onInterceptTouchEvent方法
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                //调用ViewGroup的onInterceptTouchEvent方法
                    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;
            }

清空标记之后,如果是Down手势或者mFirstTouchTarget != null 就有机会执行onInterceptTouchEvent方法,那么想一下,首次执行Down手势,肯定能进入if执行里面的代码。 disallowIntercept标记就是子View调用requestDisallowInterceptTouchEvent(boolean)设置的,我们一般处理滑动冲突是不是经常用到这个方法?!,怎么生效的呢? 慢慢看。刚开始disallowIntercept不允许父View拦截事件,那么!disallowIntercept就执行onInterceptTouchEvent(ev)方法,这就印证了刚开始我们说的View’Group执行dispatchTouchEvent之后就是执行onInterceptTouchEvent方法。

现在拦截事件也触发了,是不是该执行子ViewGroup的dispatchTouchEvent事件了?

源码接着看:

		//接下来的主要代码就是想子View'Group(或者子View)分发事件
	
		//TouchTarget 是一个链表,这个链表内存储的是子View,根据存储的子View,一步一步的
		//找到消耗事件的子View,总体来说,TouchTarget 可以理解为消耗路径。
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不取消 && 不拦截
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

		//手势是:ACTION_DOWN || ACTION_POINTER_DOWN || ACTION_HOVER_MOVE成立
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                   
                   	.....
                   
                   //newTouchTarget就是链表的头节点,跟mFirstTouchTarget会互相赋值
                   //如果子View的数量不为null
                    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 ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //循环遍历子View(ViewGroup)
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            .......
				//触摸的点不在子View内 || 触摸的子View有在执行动画
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //结束本次循环
                                continue;
                            }

				// child是否在链表中(消耗树)中,如果在,就结束for循环;
                            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);
                            // 此处是个递归调用,dispatchTransformedTouchEvent内部就是分发事件的实现,下文看源码,
                            //以下的代码核心就是:寻找消耗路径。当dispatchTransformedTouchEvent返回true的时候,也就是说有子View消耗的时候,会递归返回true,此时会将子View(ViewGroup)依次添加到链表TouchTarget中,然后返回链表的头节点:newTouchTarget。
                            //沿着这个消耗路径,也就是遍历TouchTarget的话,可以找到消耗事件的子节点。
                            //那么,如果dispatchTransformedTouchEvent返回false呢,也就是说没有子节点消耗事件呢?下文细说
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //**强调:在addTouchTarget方法内,将mFirstTouchTarget赋值,可自行查看!!!**
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //已经找到消耗路径了(事件已经被自己的子节点消耗了)
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
			
			  
                    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;
                    }
                }
            }

上述代码总体流程是这样的:
先判断是否是down手势,如果是,就for循环ViewGroup的子节点,判断触摸的点是否在子View范围内以及子View是否正在执行动画,如果在子View范围内 && 子View没有在执行动画,在看下第一个child是否在消耗路径中,如果在,就直接跳出for循环,如果不在,那么就开始一层层分发事件,知道找到消耗事件的子View(viewGroup)返回true,然后再递归更新链表(消耗路径)。那么至于事件到底是怎么分发给子节点的呢?看下dispatchTransformedTouchEvent源码:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        
	......

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            //如果child是null,就调用自己父View的dispatchTouchEvent方法,说白了就是吧ViewGroup当成了View处理
            //因为ViewGroup继承自View,所以调用的还是View的dispatchTouchEvent方法
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            .......
           //如果child不是null,就调用子节点的dispatchTouchEvent(ev); 递归调用
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

从上述代码可以看出,当for循环分发事件的时候,child是不为null的,此时调用的子节点的dispatchTouchEvent方法,深度遍历。

至此深度遍历寻找消耗路径已经完成(假设子View消耗了事件),好,想一下,现在是down手势已经分发完成了,那么move手势和up手势呢?怎么分发呢?

回头看一下上述源码,最外层先判断的就是是否是down事件,如果是down手势,上述代码的事件分发都是基于这三个手势的: ACTION_DOWN || ACTION_POINTER_DOWN || ACTION_HOVER_MOVE ,才能进入if分发事件,此时move手势和up手势怎么分发呢? 想一下,我们不是有targetTouch链表吗? 根据这个路径可以找到消耗事件的子节点。接下来的源码应该就是水到渠成了:

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            	// 没有子节点处理事件,把ViewGroup自己当成普通的View处理
            	//注意此时传递的child值为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;
                        //往下分发事件(move, up)
                        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;
                }
            }

        .......
        
        return handled;

上述源码也就是遍历链表,然后每个子节点分发事件,深度遍历,完成除了down手势之外的一系列手势。

至此我们的分析完成一部份了,回顾一下我们分析的场景:该View’Group的子节点(或者孙节点)消耗了Down手势,那么更新链表,再通过循环链表完成其它手势的分发。

好,此时考虑一个问题,如果该ViewGroup的子节点(或者孙节点)没有消耗事件, 那么也就是说Down手势判断代码里面的dispatchTransformedTouchEvent为false, 在换句话说,也就是链表为null,没有找到消耗路径,那么该怎么办呢?
还记得我们刚进入ViewGroup的时候将mFirstTouchTarget置为了null, 然后再addTouchTarget方法内,将mFirstTouchTarget赋值,那么此时根本都没有子节点消耗事件,所以addTouchTarget方法肯定进不去,那么自然而然的mFirstTouchTarget也就是null了,那么再看回头看下move和up手势的分发源码:

           // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            	// 没有子节点处理事件,把ViewGroup自己当成普通的View处理
            	//注意此时传递的child值为null,
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
            	...........
            }

当mFirstTouchTarget =null的时候,调用的是dispatchTransformedTouchEvent,其中child的参数的值是null,
再回顾一下dispatchTransformedTouchEvent源码,此时child = null, 那么就会把ViewGroup当成了普通的View处理,此时调用的是View的dispatchTouchEvent(ev)方法。然后再在dispatchTouchEvent看是否执行onTouchEvnet方法。

好了至此ViewGroup的分发事件流程分析就算完了。
总结一下流程:
1:当事件序列到达此ViewGroup时,先清空标记,也就是上一个事件序列的标记。
2:再判断是否是ViewGroup是否拦截此事件,如果拦截该事件,就交给onInterceptTouchEvent(ev)。如果没有拦截,就继续向下分发。
3:此时如果时DOWN事件,那么就会循环执行View’Group的子View, 如果子View符合包含了触摸点并且此子View没有正在执行动画,那么递归判断此子View(可能是Viewgroup),寻找是否有消耗事件的子view,如果有,就更新消耗树。
4:除了DOWN事件,那么接下来的一系列手势,都会查看是否有消耗路径,如果有,那么就循环遍历该链表,分发接下来的一系列手势事件;如果没有,那么就把此ViewGroup当作普通的View处理,此时会调用父View的dispatchTouchEvent方法。

结论:总体来说,事件分发,也就是寻找消耗路径,并遍历消耗路径的过程。

View源码分析

当ViewGroup分发流程源码中,如果ViewGroup没有消耗树,那么就会把自己当成普通的View处理:

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //注意此时传入的child是null。
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ......
        if (child == null) {
            //调用父类的dispatchTouchEvent,而父类也就是View
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
        ......
    }

通过ViewGroup中的源码,我们知道最终调用的还是View的dispatchTouchEvent, 所以接下来看下View的dispatchTouchEvent方法:

	     //核心代码:
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

上述代码是核心代码,先看下li对象,这是个ListenerInfo内部静态类,主要封装了View的功能性接口,比如:点击事件,长按事件等一系列接口对象。 比如我们设置点击事件,都是通过ListenerInfo对象调用的。从上述源码可以看出:首先判断是否设置了onTouchListener && onTouch返回了true,如果成立,那么result = true; 此时接下来的!result 也就是false,也就是说onTouchEvent方法执行不了。
因此:onTouchListener监听的方法onTouch返回值决定了是否能够执行onTouchEvent方法。

接下来看下onTouchEvent方法:

//是可点击的 || 可长点击的 || CONTEXT_CLICKABLE
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
 	|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
	|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
             if (!post(mPerformClick)) {
                   //内部执行的还是onClick方法。
                   performClick();
             }
            break;

        case MotionEvent.ACTION_DOWN:
            ......
            break;

        case MotionEvent.ACTION_CANCEL:
            ......
            break;

        case MotionEvent.ACTION_MOVE:
            ......
            break;
    }

    return true;
}
return false;

如果是可点击的 || 可长点击的 || CONTEXT_CLICKABLE, 此时就消费了事件,反之就返回false,表示不消耗事件。 从上述代码可以看书 , performClick()是在Up的时候执行的,也就是说onClick方法是在Up的时候执行的。
当然前提是可点击的 || 可长点击的 || CONTEXT_CLICKABLE。

注意:
1:View的CLICKABLE和LONG_CLICKABLE都是false;TextView的CLICKABLE是false,LONG_CLICKABLE是true;Button的CLICKABLE和LONG_CLICKABLE都是true。
2:onClick调用的前提是,View接收到了DOWN和UP事件。

实际项目中的滑动冲突原理也就是:View的事件分发机制。

Android技术交流QQ群:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lmq121210/article/details/84945416