View事件传递机制源码走查

作者: susion哒哒 | 来源:发表于2019-02-25 16:16 被阅读14次

    上一篇文章我们看了触摸事件的产生 -> Activity.dispatchTouchEvent()的整个过程。本文就从Activity.dispatchTouchEvent()为起点来看一下触摸事件是如何在View中进行分发的。触摸事件分发的源码还是比较少的。

    Activity.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); //这个方法作为Activity开始与用户触摸事件交互的回调
        }
        if (getWindow().superDispatchTouchEvent(ev)) { //交由Window处理,返回true,则代表事件处理完成
            return true;
        }
        return onTouchEvent(ev); //Window不能处理这个事件则自己处理
    }
    

    即调用了getWindow().superDispatchTouchEvent(ev),这里的Window实际上是PhoneWindow,这个方法最终会调用到DecorView(FrameLayout).dispatchTouchEvent(),这里就是View触摸事件分发的起点。

    事件的分发过程 : ViewGroup(FrameLayout).dispatchTouchEvent()

    这个方法其实并不是很长,但是也不好一次性看完。我们下面把这个方法分为3步来进行解析。

    Step1 : 是否拦截

    ViewGroup.dispatchTouchEvent()

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //在第一次派发事件(ACTION_DOWN)时,mFirstTouchTarget必定为null
            //在第一次事件(ACTION_DOWN)派发过程中子View可以请求父View不要对触摸事件做拦截。 
            //但是必须是父View在onInterceptTouchEvent中返回了false。否则子View是无法请求父View不要去拦截事件的。
            //因为它根本就没有机会
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  //ViewGroup.requestDisallowInterceptTouchEvent()可以设置这个flag
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);  //restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
    
        if(!intercepted){
            //派发事件到子View
        }else{
            //自己处理这个事件
        }
    

    mFirstTouchTarget是指在一次事件派发过程中能处理事件的某个子View。解释一下上面这段代码:

    1. 如果是ACTION_DOWN事件或者事件已经有处理者(mFirstTouchTarget!=null),那么就要判断是否拦截这次触摸事件。即确定intercepted的值是否回调onInterceptTouchEvent()
    2. 子View可以通过调用parent.requestDisallowInterceptTouchEvent()来禁止父View拦截事件的传递。但前提是父View没有对ACTION_DOWN事件做拦截, 即(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)成立

    可以用下面这张图描述是否拦截的相关逻辑:

    DispatchTouchEventStep1.png

    Step2 : 分发事件到子View

    如果第一步中intercepted==false,那么就会把这次触摸事件向子View进行分发。大致分发的逻辑是:对子View按照Z轴的顺序来派发事件(显示在最上层的子View会先接收到事件)

    ViewGroup.dispatchTouchEvent()

        //循环遍历子View,把事件派发给它们
        for (int i = childrenCount - 1; i >= 0; i--) { 
    
            final View child = ...
    
            view 是可见的才可以接收事件,如果不可见的话不会走下面的代码
    
            //如果是第一次派发事件(ACTION_DOWN),这个方法返回null。 
            //如果进行了ACTION_DOWN事件的派发,那么mFirstTouchTarget!= null, 如果child == mFristTouchTarget,则直接把事件派发给他(child)
            newTouchTarget = getTouchTarget(child);  
            if (newTouchTarget != null) {
                ...
                break;
            }
    
            //这个方法会调用子View(View/ViewGroup)的dispatchTouchEvent,看这个child是否能处理事件
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {  
                //子View可以处理这次事件
                ...
                //这个newTouchTarget就是 mFirstTouchTarget
                newTouchTarget = addTouchTarget(child, idBitsToAssign); //把 以处理这个事件的子View添加到 TouchTarget 链表中。 并把它设置为mFirstTouchTarget
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
        }
    
        这段是我写的伪逻辑:
        // 对于ACTION_MOVE事件的派发,在ACTION_DWON派发中如果已经寻找到目标child,那么会直接走这个逻辑
        if(newTouchTarget !=null && !alreadyDispatchedToNewTouchTarget){ 
            dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)
        }
    

    dispatchTransformedTouchEvent()的逻辑其实也比较简单:

    ViewGroup.dispatchTransformedTouchEvent()

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(event);   //如果本身是`ViewGroup`,那么这里实际上调用的是View.dispatchTouchEvent()
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
    
            //派发给子View前 对坐标进行了转换
            event.offsetLocation(offsetX, offsetY);
    
            handled = child.dispatchTouchEvent(event); //调用子View的 dispatchTouchEvent()
    
            //派发给子View后 再把坐标转换回来
            event.offsetLocation(-offsetX, -offsetY);
        }
        ...
    }
    

    其实我感觉用语言并不是很好描述,还是看图吧:

    DispatchTouchEventStep2.png

    Step3 : 没找到事件处理者,自己处理这次事件。View.dispatchTouchEvent()

    这段逻辑对应的是下面这段代码:

    ViewGroup.dispatchTouchEvent()

        if (mFirstTouchTarget == null) { //没有谁能够处理这个事件,交由自己来处理   
            //传null, 会调用 super.dispatchTouchEvent(event)。
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } 
    

    这里super.dispatchTouchEvent(event)实际上是调用到了View.dispatchTouchEvent():

    View.dispatchTouchEvent()

        public boolean dispatchTouchEvent(MotionEvent event) {
            ...
    
            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;
            }
    
            ...
    
            return result;
        }
    

    即会依次调用View的 : i.mOnTouchListener.onTouch(this, event)onTouchEvent(event)

    ok,到这里,分析完了ViewGroup.dispatchTouchEvent()的全逻辑。那触摸事件分发过程中,事件是如何消费和传递的呢?(上面代码中我没有展示事件消费相关代码),可以结合下面这张图,来理解触摸事件传递过程中是如何被消费的:

    触摸事件分发流程图.png

    图出自 : https://blog.csdn.net/binbinqq86/article/details/82315399

    源码走查后的一些小结论

    如果在一次事件派发中没有一个子View对事件做了处理,那么就不会再派发事件。全部事件由自己来处理。

    其实就对应着上面Step2中,如果对子View遍历后mFirstTouchTarget还是为null,那么就自己来处理这个事件:

        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { 
    
        }else{
            intercepted = true
        }
    
        if(intercepted){
            //自己处理
        }
    

    如果子View处理了触摸事件,那么后续的事件都会派发到这个子View,不会派发给其他子View处理了

    Step2中,在变量子View时,如果发现子View就是mFirstTouchTarget,那么就会跳出循环,直接把事件派发给这个子View

        for (int i = childrenCount - 1; i >= 0; i--) { 
        newTouchTarget = getTouchTarget(child); //判断child是否是上次派发事件过程中的target
        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;
        }
        }
    
        把事件派发给 newTouchTarget所指的child
    

    关于 onInterceptTouchEvent

    如果这个方法返回了false, 并且父View没有处理触摸事件,事件被子View处理。那么每次都会回调这个方法,并且将事件分发到子View中。

    如果父View在ACTION_DOWN事件的下发过程中,onInterceptTouchEvent就返回了true,那么事件是永远不可能派发到子View的。

    如果子View接收到了ACTION_DOWN事件,那么它是可以通过parent.requestDisallowInterceptTouchEvent()来使父View不拦截事件的下发的。

    如果我们想处理一个ViewGroup的所有事件, 我们应该重写dispatchTouchEvent,不要重写onInterceptTouchEvent

    相关文章

      网友评论

        本文标题:View事件传递机制源码走查

        本文链接:https://www.haomeiwen.com/subject/avygyqtx.html