Android MotionEvent事件分发介绍与流程总结(伪代码形式)

    如果要一句话简单总结的话,就是:
    找到一个按照规则“消耗”掉MotionEvent.ACTION_DOWN事件的View,默认情况下,后继会把整个事件流都交给它来处理。
#.总体概括
     Android手机是可触屏的设备,其它Android设备一般也是可触屏的。
    可触屏设备允许用户与屏幕进行一些触碰的互动,系统识别各式各样的触摸操作,然后做出复杂的功能反应。
    本文一切都是针对Android手机来分析说明的。
    用户手机触摸屏幕的那一瞬间,Android系统就会把这一次的触屏操作相关信息封装成一个MotionEvent对象,用来描述本次触屏操作,包括触屏操作的类型、发生的时刻、对应的位置坐标等。
    每一轮触屏操作,总是以手指按下屏幕开始(ACTION_DOWN),手指在屏幕上可以进行各种轨迹的滑动(ACTION_MOVE),然后手指松开屏幕结束这一轮触屏操作(ACTION_UP)。
    在这个过程中,每一次系统识别到了新的触屏操作(按下/滑动/松开),都会封装对应的MotionEvent对象,这一轮的所有事件叫做一个“事件流”。“事流”这个概念在分析事件具体分发的时候会用到。(当然,每个人的叫法不同。)
    一个事件流中的事件,会按照View树的结构,分析判断并最终传递给指定的View目标来处理,或者最终没有任何目标View处理,最终交给Activity、Dialog这种最顶层的组件来处理。
    正常情况下,一个“事件流”只会交给一个目标View来处理,谁消耗了整个事件流的起始事件——按下事件(MotionEvent.ACTION_DOWN),谁就是目标View,后面整个事件流的所有事件都会逐层传递给它来处理。(之所以说正常情况,是因为开发者可以干预这个过程,通过继承并修改事件传递和拦截的方法,可以在原目标View的更上层ViewGroup拦截事件,将事件流的后继事件交给更上层ViewGroup来处理。)
    “按下”(MotionEvent.ACTION_DOWN)事件非常关键,它决定了谁是整个事件流的目标View。
    MotionEvent.ACTION_DOWN事件的整个分发过程是按照“责任链模式”来设计的,按照View树结构,从顶层开始先向下一层一层的链式分发,一直分发到View树叶子节点这一层 或者 中间某一层ViewGroup拦截了事件,然后从这一层开始从下往上回溯,回溯的过程中每一层ViewGroup会判断下层子View是否“完成了责任"(即消耗了事件),没消耗的话自己回去尝试“完成责任”,最后通过返回值告诉自己的上一层View:自己这个View树分支是否“完成了责任”。上一层View又会重复这个过程,周而复始,若是没有任何一层来“完成责任”,最后会交给Activity、Dialog这些最顶层的组件来处理。
#.MotionEvent介绍
    Android将所有触屏手势事件都封装在了MotionEvent对象中。
1.每种事件有其对应的类型,常见的事件类型有:
        MotionEvent.ACTION_DOWN     // 第一个触点被按下时触发(因为可能有多个手指去按屏幕)
        MotionEvent. ACTION_MOVE     // 有触点移动时触发
        MotionEvent. ACTION_UP          // 最后一个触点放开时触发
        MotionEvent.ACTION_CANCEL  //当前事件流被取消,该类型事件不是由用户操作触发的,而是分发处理逻辑在一定场景下触发的。一般触发场景是:某一个View消耗了MotionEvent.ACTION_DOWN成为该事件流的目标View,但后面按照开发者写的特定拦截逻辑,在上层某个ViewGroup拦截了事件流中的后继事件,那么这时候会触发一次 MotionEvent.ACTION_CANCEL事件,传递给原来的目标View。
        MotionEvent.ACTION_POINTER_DOWN    //多个触点时,按下非第一个点时触发
        MotionEvent.ACTION_POINTER_UP    //多个触点时,松开非最后一个点时触
2.可通过 getAction()、getActionMasked()获取事件类型   
getAction()返回的值,高8位是触点索引值 pointerIndex ,低8位是事件类型,对于第一个触点,触点索引值 pointerIndex为0。
getActionMasked()返回的只有 getAction()返回值中的低8位,对应事件类型。
所以,当只有一个触点时, getAction()与 getActionMasked()返回值相同;当有多个触点时,应该使用 getActionMasked()。 
 
3.获取事件发生位置坐标
1.事件点相对于屏幕的坐标,可通过MotionEvent对象的以下API获取:
     getRawX()
    getRawY() 
2.事件点相对于当前View的坐标,可通过MotionEvent对象的以下API获取:
(MotionEvent对象是从上层一级一级传递过来的,当传递到某个View时,可调用相应API获取到相对于该View的位置坐标)
    getX()  
    getY()
#.事件分发和处理流程
1.事件分发和处理关联的三个关键方法简介:
1.1 public boolean dispatchTouchEvent (MotionEvent event)
    由父View来调用,用来分发事件。如果一个View的dispatchTouchEvent()方法被调用,说明事件已经被分发到该View。
    返回值ture/false:告诉父View,以子View为根节点的View树分支 是否“消耗”了该事件。
1.2 public boolean onInterceptTouchEvent (MotionEvent event) 
    只有ViewGroup才有该方法,View类中未定义该方法,因为非ViewGroup的View一般充当叶子节点,无需拦截功能。
    判断当前ViewGroup是否拦截事件。如果拦截,自顶而下的事件分发到此为止。而且,只要针对某个事件流中任意事件拦截一次,后继整个事件流都不会再传递到该ViewGroup更下层,其onInterceptTouchEvent()也不会再次调用。
    返回值ture/false:当前ViewGroup是否拦截该事件,默认是返回false,即不拦截。
3. public boolean onTouchEvent (MotionEvent event) 
    该方法负责针对事件来做对应逻辑处理,即负责“消耗”掉事件。
    返回值ture/false:当前View是否消耗了该事件。
2.从Activity等组件传递到View树祖先节点DecorView
    当用户进行触屏操作后,经过一系列底层处理,最终会调用Activity或Dialog这些组件的dispatchTouchEvent(MotionEvent event)方法,它们会调用内部Window的dispatchTouchEvent方法,这个Window的具体实现类是PhoneWindow,PhoneWindow又会调用内部顶层View即DecorView的dispatchTouchEvent方法。DecorView继承自FrameLayout,是一个ViewGroup,传递到DecorView,后面就会按照View树结构逐层往下传递。
3.ViewGroup中的分发处理流程
阅读源代码,忽略掉细节(如多个触点相关逻辑),整个分发流程大体可以总结为以下伪代码:
//一个单链表,用来记录当前ViewGroup应该把事件传递给哪个子View
//因为可能有多个触点,所以要用一个链表来记录
private TouchTarget mFirstTouchTarget;
//事件分发方法的大致逻辑
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;//是否消费事件

    if(事件类型 == MotionEvent.ACTION_DOWN){
        清空之前的各种相关状态和记录,例如清空mFirstTouchTarget中的值,恢复默认状态让ViewGroup不拦截事件;
        //因为ACTION_DOWN意味着一个新的事件流的开始,需要清除之前的状态,恢复到默认值。
        //而且一个事件流依靠ACTION_DOWN来选定最终的目标View,
        //这里清空所有相关状态,恢复默认值,也能保证ACTION_DOWN按需要尽可能往下传递而不被拦截。
    }

    /*************    拦截的相关判断和调用      *************/
    final boolean intercepted;//是否拦截
    if (事件类型 == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        //如果是ACTION_DOWN时,或者之前已经在ACTION_DOWN时发现了下一层目标View需要判断是否拦截
        //其中,ACTION_DOWN时,因为前面已经将所有状态都恢复为了默认值,而默认是允许拦截的,所以一定会走到if分支内部
        if (允许当前ViewGrop拦截) {
            //允许拦截时,不会代表就会拦截,还要看ViewGroup是否需要拦截
            //所以要去调用ViewGrup的onInterceptTouchEvent()
            intercepted = onInterceptTouchEvent(ev);
        } else {
            //不允许拦截时,直接设置为false
            intercepted = false;
        }
    } else {
        //走到这里,说明没有目标子View,而且是事件流的后继事件(不是第一个事件ACTION_DOWN)
        //那么一定是需要在本层拦截的,没法交给下一层
        intercepted = true;
    }

    /*************    ACTION_DOWN时去寻找下一层的目标View      *************/
    boolean canceled = ...判断是否应该取消事件...;
    //去遍历子孙节点,寻找哪个子View对应的树分支消耗了事件,
    //那么这个子View将会被添加到mFirstTouchTarget中,mFirstTouchTarget将不再是null
    //注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况
    if(!canceled && !intercepted && 事件类型 == MotionEvent.ACTION_DOWN){
        //只有不取消、不拦截,而且事件类型是MotionEvent.ACTION_DOWN,才会去寻找下一层的目标View
        //其它情况下,要么
        for(遍历子View){
            if(该子View不应该接受事件,例如事件位置在View的范围外等){
                continue;
            }
            //分发事件给子View
            //该方法内部会调用子View的dispatchTouchEvent()
            //第二个参数表示是否取消事件流,若为true,则分发给子View的事件类型会被修改成MotionEvent.ACTION_CANCEL
            //此处第二个参数固定为false
            if(dispatchTransformedTouchEvent(ev, false, 子View, ...)){
                //走到这里,说明子View树消耗了事件
                修改mFirstTouchTarget,将该子View设置为下一层的目标View;
                break;
            }
        }
    }

    /*************      非ACTION_DOWN时的处理      *************/
    //注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况
    if (mFirstTouchTarget == null) {
        //如果没有找到下一层的目标View,则交给自己来处理,第三个参数为null时会交给自己来处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null, ....);
    } else {
        //如果找到了下一层的目标View,则传递给已经找到的目标来处理
        if(当前事件已经在前面的的逻辑流程中被消耗,只有ACTION_DOWN类型且寻找到下一层目标时会如此){
            handled = true;//标明事件被消耗
        } else {
            //如果拦截,则通知子View取消事件流
            boolean cancelChild = intercepted;
            //将事件传递给前面已经找到的下一层目标View
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, ...)) {
                //子View树消耗了事件
                handled = true;
            }
            if(cancelChild){
                清除mFirstTouchTarget的值;
                //如果ViewGroup拦截,会清除其mFirstTouchTarget的值
                //所以非ACTION_DOWN事件下次再分发到该ViewGroup时,会直接走到对应判断分支
                //  设置intercepted=true,并且不会再次调用onInterceptTouchEvent(ev)
            }
        }
    }
}

//上面dispatchTransformedTouchEvent()的大致逻辑
//注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                              View child, int desiredPointerIdBits) {

    boolean handled = false;//是否消耗事件
    if(cancel || event是MotionEvent.ACTION_CANCEL类型){
        将event类型设置为ACTION_CANCEL;
        if (child == null) {
            //调用View类的dispatchTouchEvent(),最终内部根据情况能够调用onTouchEvent()
            //ViewGroup类并未定义自己的onTouchEvent()方法
            handled = super.dispatchTouchEvent(event);
        } else {
            //调用子View的dispatchTouchEvent(event)
            handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }

    根据event来新建MotionEvent对象,并调整中间的一些值,例如相对View的偏移坐标等;
    if (child == null) {
        //调用View类的dispatchTouchEvent(),最终内部根据情况能够调用onTouchEvent()
        //ViewGroup类并未定义自己的onTouchEvent()方法
        handled = super.dispatchTouchEvent(新的MotionEvent对象);
    } else {
        //调用子View的dispatchTouchEvent(event)
        handled = child.dispatchTouchEvent(新的MotionEvent对象);
    }
    return handled;
}
4.View类中的分发处理流程
阅读源代码,忽略掉细节,整个分发流程大体可以总结为以下伪代码:
//根据以上分析,如果开发者不覆盖方法重写代码,最终其实会调用到View类的dispatchTouchEvent(event)
//有可能是某个View本身不是ViewGroup类型所以调用到,也可能是在dispatchTransformedTouchEvent()方法中
//  交给ViewGroup来处理事件,它调用了继承自View类的dispatchTouchEvent(event)
//View类的dispatchTouchEvent()大致逻辑
//注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;//是否消耗事件
    if (View未被禁用 && 事件未被当做滚动事件) {
        result = true;
    }
    if (设置了OnTouchListener && mOnTouchListener.onTouch(this, event))
        result = true;
    }
    if (事件到此还未被消耗 && onTouchEvent(event)) {
        result = true;
    }
    return result;
}

public boolean onTouchEvent(MotionEvent event) {
    //CLICKABLE/LONG_CLICKABLE/CONTEXT_CLICKABLE只要允许任意一项,该值都是true
    boolean clickable = ...是否可点击...;

    if (View被禁用) {
        //不会处理时间,但只要可点击,会直接返回true,告诉上层已消耗事件
        return clickable;
    }

    if (clickable) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                .......
                if(可点击并且设置了OnClickListener){
                    执行OnClickListener.onClick(当前View);
                }
                .......
                break;
            .......
        }

        return true;
    }
    return false;
}
5.一些补充
ViewGroup提供了public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,
用于设置是否关闭ViewGroup的事件拦截,即传入true时关闭拦截,传入false时允许拦截。
当调用某个ViewGroup的该方法后,会沿着View树不断向上调用父View的requestDisallowInterceptTouchEvent(),开启或者关闭拦截。
#.根据源码中的分发处理流程,可以得到的一些结论和应用场景
##.一些结论
1.一个事件流的ACTION_DOWN事件很关键,在寻找它的消耗者View过程中,每一层ViewGroup都记录了下一层应该把事件传递给谁,于是事件流后面的事件再次分发时,只需要按照之前记录的路径链条,就能分发给消耗掉ACTION_DOWN的View。
2.处理ACTION_DOWN事件时,在ViewGroup.dispatchTouchEvent()中会首先清除前面的相关状态,恢复到默认状态。因此对ViewGroup设置requestDisallowInterceptTouchEvent(true),在下一次事件流到来时会失效,因为默认是允许拦截的。
3.处理非ACTION_DOWN事件时,如果ViewGroup拦截,会清除其mFirstTouchTarget的值,并且向原来的目标View发送一次ACTION_CANCEL类型的事件。
事件流中后面非ACTION_DOWN事件再分发时,分发到该ViewGroup时,会直接走到对应判断分支设置intercepted=true,并且不会再次调用其onInterceptTouchEvent()。无论该ViewGroup每次是否消耗事件,每次都会分发到它这里,因为更上层的mFirstTouchTarget并未被清空,记录着有效的目标。
4.在处理事件时,如果设置了OnTouchListener并且在其onTouch()回调中消耗了事件,将不会再调用View的onTouchEvent()。
5.View的onTouchEvent()中,只要当前View是可点击的一定会返回true,代表着消耗了事件。
   但如果当前View被setEnabled(false),即被禁用,不会做任何有效处理,就会按照当前View是否可点击,来直接返回true或false。 
   如果View设置了OnClickListener,是经由onTouchEvent()来触发器其对应回调的。
##.应用场景举例
1.解决滑动冲突,例如ViewGroup是上下滑动的,内部View是左右滑动的
一开始由内部View消费ACTION_DOWN,处理事件流。一旦轨迹方向与水平方向超过一定角度,例如45度,就判定为上下滑动,由ViewGroup拦截事件流,负责后继处理。
2.RecyclerView的上拉刷新、下拉刷新动画
一开始由RecyclerView消费ACTION_DOWN,处理事件流。但当滑动到RecyclerView最顶部或最底部还继续滑动时,就由上层ViewGroup拦截事件流,负责后继处理,展示对应的UI效果。

(声明:部分图片获取自网络,这里只是用于学习分享,侵删!) 

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/124593154
今日推荐