Android事件分发机制

作者: 酱爆大头菜 | 来源:发表于2019-11-12 18:06 被阅读0次

    事件分发机制是一个老生常谈的问题了,正常的开发中经常会遇到,比如滑动冲突。而事件分发也经常在面试中问到。
    本文参考了以下文章,并借用了部分图表,感谢各位大神的分享。

    郭霖 的 Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    郭霖 的 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

    Carson_Ho 的 Android事件分发机制详解:史上最全面、最易懂

    九号锅炉 的 Android-从重叠view响应问题到安卓事件分发机制

    接下来的文章将从事件分发的用途,事件的类型,事件分发的顺序,事件分发的流程,以及对应举例等5个方面去方位解析事件分发机制,关键部分以将源码的调用流程进行了详细说明。


    事件分发机制是用来干什么的?
    • 首先Android系统处理用户个各种行为是通过一个个事件(MotionEvent)来驱动的,而事件分发简单来说就是分配这些事件应该由谁来处理和消费。
    • 这时候可能有人又问了,你点哪个组件就由哪个组件来处理就好了,用不上什么分发啊,只需要知道被点击的组件是谁不就可以了么?
    • 那么问题来了,重叠的组件怎么办呢?点击后所有重叠组件都可以响应,那到底由谁来处理这个事件?又或者像滑动列表这种组件呢?明明你点的是每个item,但是列表也在因你的行为在滚动,带着这一系列的疑问咱们往下看。

    事件都有哪些?

    在用户的正常操作中,主要用三种事件来描述用户整个的行为,分别为

      1. 按下:MotionEvent.ACTION_DOWN
      1. 滑动:MotionEvent.ACTION_MOVE
      1. 抬起:MotionEvent.ACTION_UP
      1. 还有一个事件用来描述非用户行为的事件退出:MotionEvent.ACTION_CANCEL。

    以上的按下,滑动,抬起三个事件非常好理解,不再过多解释。那么最后这个MotionEvent.ACTION_CANCEL到底什么什么意思,又是在什么情况下出现的呢?接下来用一个实际的场景进行描述。

    • 现在有以下伪代码布局:
    <RecyclerView>  //一个RecyclerView布局
          <LinearLayout>  //代表RecyclerView的每个Item
                <Button />
          </LinearLayout>    
    </RecyclerView>
    
    • 首先我们点击屏幕后开始滑动,首先会监听到了Button的 ACTION_DOWN触发,紧接着RecyclerView发现是一个滑动事件,拦截了Button的后续事件。
    • 此时会触发一个ACTION_CANCEL事件,本应将ACTION_MOVE也分发给Button,但是被父布局强制拦截,只能将ACTION_CANCEL分发给Button来告诉Button你的事件被拦截了,连后续的ACTION_UP也不会分发给你了
    • 简单来说,当Button收到ACTION_CANCEL事件的时候代表他的整个事件流结束了。

    一张图描述4种事件的调用关系

    事件流程.png
    事件分发的顺序是怎样的?
    • 先上一张图描述他们的层级关系。


      层级关系.jpg
    • 事件的分发顺序为:Activity--->PhoneWindow--->DecorView--->ViewGroup--->View

    • Activity:无需解释.

    • PhoneWindow:是抽象类Window的实现类,抽象类Window是所有视图的顶层容器,View的外观和行为均由他进行管理。

    • DecorView:PhoneWindow的内部类,是Activity的根View,继承于FramLayout。PhoneWindow通过DecorView传递信息给底层View,而底层View也通过DecorView返回消息给PhoneWindow。

    • ViewGroup:即为类似于LinearLayout,FrameLayout,ListView等可包含多个View的组件,同时自己也是个View,因为ViewGroup 也继承了View,只是包含了子View定义布局参数的功能。

    • View:所有UI组件的基类,Button,TextView等单一的组件均继承View。

    对此很多同学对PhoneWindow和DecorView比较陌生,这两个对象到底是什么鬼,因此稍稍补充一点布局加载的知识。

    • 1. Activity在onCreate()中执行setContentView(),内部执行的其实是PhoneWindow.setContentView(),而在PhoneWindow内部完成了DecorView的创建。
    • 2. DecorView将屏幕分为两个部分,titleView和contentView,平时加载的布局即时contentView。

    事件分发的详细流程是什么样的?

    在整个事件分发过程中,主要依赖三个方法

      1. dispatchTouchEvent()
      1. onInterceptTouchEvent() //该方法仅存在于ViewGroup中
      1. onTouchEvent()。

    首先贴一张事件分发的总图

    事件分发总结.png
    • 接下来将从源码的角度分析整个事件分发的详细流程,代码中我做了详细的注释。
    • 代码中的所有缩进都代表上一个方法的内部逻辑明细,没有缩进的平行方法都代表是平行顺序调用,没有包含嵌套关系,以下的源码分析可通过层级关系进行详细阅读。
    --------------------------------------------------------
    --------------Activity的事件分发------------------------ 
    // 事件产生后调用入口
    Activiry.dispatchTouchEvent()
    
        --------------------------------------------------------
        --------------以下为ViewGroup的事件分发------------------ 
        //事件交给ViewGroup去处理,返回true说明事件被消费,无需执行Activity.onTouchEvent()
        if(ViewGroup.dispatchTouchEvent()) {return true}
            //disallowIntercept代表是否禁用拦截功能,默认是false,可通过调用requestDisallowInterceptTouchEvent()修改。
            //onInterceptTouchEvent(ev)代表拦截器, !onInterceptTouchEvent(ev)值为true代表不拦截,false代表拦截。
            //onInterceptTouchEvent()默认返回false,可通过复写该方法修改返回值。
            if(disallowIntercept || !onInterceptTouchEvent(ev))
                    //当前ViewGroup没有拦截当前事件
                    //循环遍历当前ViewGroup的所有子View,通过x,y坐标找到被点击的View
                    //调用子View的dispatchTouchEvent()并依赖返回值返回ViewGroup.dispatchTouchEvent()方法
                    if (child.dispatchTouchEvent(ev)){ return true }
    
                             --------------------------------------------------------
                             --------------以下为View的事件分发------------------ 
                            //mOnTouchListener即为当前View是否setOnTouchListener()。
                            //(mViewFlags & ENABLED_MASK) == ENABLED 当前控件是否启用。
                            //mOnTouchListener.onTouch()值为复写的onTouch方法返回值是否为true。
                            //以上条件全部成立则返回true代表事件已消费
                            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  mOnTouchListener.onTouch(this, event)){ 
                                return true 
                            }
                            //以上条件有一个不成立则调用View.onTouchEvent()
                            return View.onTouchEvent();
                                    //如果当前View可点击(CLICKABLE或LONG_CLICKABLE状态),则进入switch。
                                    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
                                           switch (event.getAction()) { 
                                                case ACTION_UP:
                                                    //如果当前为抬起事件则调用performClick()
                                                     performClick();  
                                                         //如果mOnClickListener != null,mOnClickListener值通过View.setOnClickListener()赋值。
                                                         //由此可见onClick事件是通过ACTION_UP触发的,但是在此触发之前,onTouch事件已经触发,因此onTouch早于onClick。
                                                         if (mOnClickListener != null) { 
                                                            mOnClickListener.onClick(this);  
                                                            //返回到View的dispatchTouchEvent()方法中。
                                                            return true;
                                                         }
    
                                                case ACTION_DOWN:
                                                case ACTION_CANCEL:
                                                case ACTION_MOVE:
                                           }
                                           //只要当前View是可点击的,则返回true;
                                           return true;
                                    }
                                    // 若该控件不可点击,就一定返回false
                                    return false;
    
            //如果拦截了该事件或者用户点击到了空白处(未点到控件),则调用ViewGroup父类的的dispatchTouchEvent(),即View.dispatchTouchEvent()。
            return super.dispatchTouchEvent(ev);    
    
    //没有控件处理,Activity自己处理
    Activity.onTouchEvent()
        //主要处理当前点击是否在边界外,true说明事件在边界外,即 消费事件。false则说明未消费。
        PhoneWindow.shouldCloseOnTouch()?return ture:false      
    
    

    以上源码分析中,Activiry.dispatchTouchEvent()内写的调用ViewGroup.dispatchTouchEvent(),可明明真正源码中调用的是getWindow().superDispatchTouchEvent(ev),为什么写成了ViewGroup.dispatchTouchEvent()?

      1. 其实Activiry.dispatchTouchEvent()内调用的确实是getWindow().superDispatchTouchEvent(ev), 而getWindow()是用来获取Window类的对象,且Window是一个抽象类,其唯一实现类是PhoneWindow,因此getWindow() = PhoneWindow,而getWindow().superDispatchTouchEvent(ev) = PhoneWindow.superDispatchTouchEvent()。
      1. PhoneWindow.superDispatchTouchEvent()后续调用 -> DecorView.superDispatchTouchEvent() -> DecorView.super.dispatchTouchEvent()。
      1. 而DecorView继承自FrameLayout,是所有界面的父类,而FrameLayout又是ViewGroup的子类,故DecorView的间接父类是ViewGroup,所以DecorView.super.dispatchTouchEvent() = ViewGroup.dispatchTouchEvent(),从而论证getWindow().superDispatchTouchEvent(ev) = ViewGroup.dispatchTouchEvent()

    相关文章

      网友评论

        本文标题:Android事件分发机制

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