事件分发机制是一个老生常谈的问题了,正常的开发中经常会遇到,比如滑动冲突。而事件分发也经常在面试中问到。
本文参考了以下文章,并借用了部分图表,感谢各位大神的分享。
郭霖 的 Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
郭霖 的 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
Carson_Ho 的 Android事件分发机制详解:史上最全面、最易懂
九号锅炉 的 Android-从重叠view响应问题到安卓事件分发机制
接下来的文章将从事件分发的用途,事件的类型,事件分发的顺序,事件分发的流程,以及对应举例等5个方面去方位解析事件分发机制,关键部分以将源码的调用流程进行了详细说明。
事件分发机制是用来干什么的?
- 首先Android系统处理用户个各种行为是通过一个个事件(MotionEvent)来驱动的,而事件分发简单来说就是分配这些事件应该由谁来处理和消费。
- 这时候可能有人又问了,你点哪个组件就由哪个组件来处理就好了,用不上什么分发啊,只需要知道被点击的组件是谁不就可以了么?
- 那么问题来了,重叠的组件怎么办呢?点击后所有重叠组件都可以响应,那到底由谁来处理这个事件?又或者像滑动列表这种组件呢?明明你点的是每个item,但是列表也在因你的行为在滚动,带着这一系列的疑问咱们往下看。
事件都有哪些?
在用户的正常操作中,主要用三种事件来描述用户整个的行为,分别为
- 按下:MotionEvent.ACTION_DOWN
- 滑动:MotionEvent.ACTION_MOVE
- 抬起:MotionEvent.ACTION_UP
- 还有一个事件用来描述非用户行为的事件退出: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。
事件分发的详细流程是什么样的?
在整个事件分发过程中,主要依赖三个方法
- dispatchTouchEvent()
- onInterceptTouchEvent() //该方法仅存在于ViewGroup中
- onTouchEvent()。
首先贴一张事件分发的总图
- 接下来将从源码的角度分析整个事件分发的详细流程,代码中我做了详细的注释。
- 代码中的所有缩进都代表上一个方法的内部逻辑明细,没有缩进的平行方法都代表是平行顺序调用,没有包含嵌套关系,以下的源码分析可通过层级关系进行详细阅读。
--------------------------------------------------------
--------------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()?
- 其实Activiry.dispatchTouchEvent()内调用的确实是getWindow().superDispatchTouchEvent(ev), 而getWindow()是用来获取Window类的对象,且Window是一个抽象类,其唯一实现类是PhoneWindow,因此getWindow() = PhoneWindow,而getWindow().superDispatchTouchEvent(ev) = PhoneWindow.superDispatchTouchEvent()。
- PhoneWindow.superDispatchTouchEvent()后续调用 -> DecorView.superDispatchTouchEvent() -> DecorView.super.dispatchTouchEvent()。
- 而DecorView继承自FrameLayout,是所有界面的父类,而FrameLayout又是ViewGroup的子类,故DecorView的间接父类是ViewGroup,所以DecorView.super.dispatchTouchEvent() = ViewGroup.dispatchTouchEvent(),从而论证getWindow().superDispatchTouchEvent(ev) = ViewGroup.dispatchTouchEvent()
网友评论