android事件分发机制的学习告一段落,先写篇文章做个总结,如有新的认识,后续再进行补充。
首先从两个问题引出android 的事件分发机制:
如下图,绿色部分A代表应用的一个填充父窗体的view对象,B 是 A 的子view,C 是 B 的子view,D 又是 C 的子view。
1、如果我们点击了D中有手势标注的地方,那么,A、B、C 和 D 中到底可以有几个view对象响应此次事件?
从我们的实际需求分析,用户在点击屏幕时都有明确的目的 —— 长按一个图标、点击一个button或是一个区域(比如一个LinearLayout),好像不太可能有用户在点击 D 视图的时候希望 D 及其父视图 C 同时做出响应吧!所以,这个问题的答案很明确,当用户点击屏幕上的某一个点时,只能是包含这个点的所有view对象中的一个来做出响应。
2、接着上边的问题 —— 当我们点击了 D 中有手势标注的地方,A、B、C 和 D 中到底由哪个view对象来响应此次事件?
针对这个问题,可以有两种解决方案:
第一种解决方案:从包含点击坐标的最小view对象开始,看这个view对象是否响应了此次事件,如果响应了,此次事件结束,如果没有响应,看它的父view是否响应,如果它的父view响应了,此次事件结束,如果它的父view没有响应,看它的父view的父view是否响应 ... ... ,直到应用程序的根view。
第二种解决方案:从应用程序的根 view 开始,遍历所有包含点击坐标的 view 对象,直到某一个 view 对象响应了此次事件。
一般来说,我们应该优先让最里层的view响应事件。
第一种解决方案,貌似可以,但它的处理逻辑不太好,不如第二种从最外层进行遍历的好。
第二种解决方案呢,处理逻辑是好的,比如在android的源码中,我们就可以发现很多类似的遍历做法,比如 view 的measure、layout 等。但是第二种解决方案有一个问题,如果从根view开始,遍历到B的时候,事件被B消费掉了,那C和D岂不是连发生了什么事件都不知道了,这种解决方案相当于是让外层的view对象优先响应事件,与我们的需求不符合。
那怎么办呢?
我们将两种解决方案揉在一起,从应用程序的根view开始遍历,先(不作处理)将事件派发给包含点击坐标的所有view对象,直到最里层。然后看最里层的view消不消费,不消费再往外层派发。
这就是android事件分发机制的大体描述,在详细分析之前,我们先来处理 view对象响应事件的问题
我们知道,常用的事件有 onClick、onLongClick ,可是这两种(包含其他很多常用的事件 —— 暂且把它们叫做组合事件)都是由单一事件 down、move 和 up 组合而成的。
当用户触摸到屏幕,可能是想调用onClick方法,也有可能是想在接触屏幕的瞬间记下触摸点的坐标(响应DOWN事件)然后在移动过程中根据新的坐标来做一些其他处理(响应MOVE事件),也有可能是想长按一个view。
所以,view对象响应事件的问题 大致可以等同于 处理 view 的 down、move、up、 onClick 和 onLongClick 事件的问题。
那怎么处理 down、move、up、 onClick 和 onLongClick 事件的关系呢?android 中的做法是:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
提供
OnTouchListener 接口,让码农们在onTouch方法中来处理单一事件down、move和up(
当然也可以重写
onTouchEvent方法来处理
),在
onTouchEvent方法中将
down、move和up组合成
onClick 和 onLongClick 事件(相关的源码分析网上有很好的文章可以参考,这里就不详细写了)。并且
onTouch方法
优先于
onTouchEvent方法被执行
,
onTouchEvent方法是否被执行又取决于
onTouch方法的返回值,
这是比较巧妙的,从事件发生的时间上来看,down事件刚发生时,是不可能触发
onClick 和 onLongClick
的,move发生之后,也就不可能产生
onLongClick了,而up发生后,才可能会有onClick。
码农们可以让
onTouch方法返回true,
表示我只需要处理单一事件,不用再把
down、move和up组合成
onClick 和 onLongClick
。这种情况下,
dispatchTouchEvent方法返回true
,表示事件被响应了。
如果
onTouch方法返回false,才再去执行
onTouchEvent方法,
这种情况下
dispatchTouchEvent方法的返回值就是
onTouchEvent方法的返回值,只有
onTouchEvent方法返回true才代表此次事件被响应了
。
接下来是onTouchEvent方法返回值的问题。
源码就不贴了,比较长,而且网上有不少分析得比较好的文章,这里就写结论吧:
我们在上文中说过,onTouchEvent方法的主要作用就是将down、move和up组合成onClick 和 onLongClick 组合事件。所以,一个clickable或者longclickable的View在onTouchEvent方法中是一定会返回true的,而一般的View既不是clickable也不是longclickable的(Button是clickable的),我们可以通过setClickable()或setLongClickable()来设置View为clickable或longClickable,或者如果我们为view设置了OnLongClickListener()或OnClickListener(),该view在onTouchEvent方法中也是会返回true的。
所以,综上所述,下列两种情况下,表示一个view对象响应了一次事件 :
一、设置了OnTouchListener,onTouch方法返回true
二、没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false,并且onTouchEvent方法返回true(当View为clickable或longClickable)
在处理完view的事件响应问题之后,我们来分析android的事件分发机制
当我们点击了屏幕,就会触发Activity的dispatchTouchEvent方法,见源码:
<span style="font-family:Microsoft YaHei;font-size:14px;background-color: rgb(255, 255, 255);">public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// public void onUserInteraction(){}是一个空方法
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}</span>
getWindow().superDispatchTouchEvent()方法最终调用的是Window的子类PhoneWindow的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(KeyEvent event) {
return mDecor.superDispatchTouchEvent(event);
// 执行的是DecorView类的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
// 执行的是DecorView的父类FrameLayout的dispatchTouchEvent方法
return super.dispatchTouchEvent(event);
}
}
(关于DecorView及应用窗口层级关系参考Android - View的绘制流程一(measure))
我们看到,DecorView的SuperDispatchTouchEvent方法执行的是其父类FrameLayout中的dispatchTouchEvent()方法,
而FrameLayout中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法
(源码经过简化,只保留大致逻辑):
接收到一个ACTION_DOWN事件时,第3行的条件判断成立,于是:
第4到第8行,将mMotionTarget 设为null
第11行,判断是否为 不允许拦截或者允许拦截但不拦截,如果成立,则:
遍历子view,如果子view满足 VISIBLE 或者 正在执行动画 两个条件中的一个,进一步判断子view是否包含触摸点坐标,如果包含,则在第22行调用子view的dispatchTouchEvent()方法。
如果遍历完都没有哪个子view的dispatchTouchEvent()方法返回true,则代表ACTION_DOWN事件没有得到任何子view的响应,在这种情况下,就不会再接收到ACTION_MOVE和ACTION_UP事件了,mMotionTarget也就为null,于是target为null,执行39行DecorView的父类view的dispatchTouchEvent()方法,上文已经分析过,如果一个view没有设置OnTouchListener或设置了OnTouchListener但是onTouch方法返回false,并且这个view既不是clickable也不是longclickable的话,执行到39行就会返回false,于是Activity的onTouchEvent方法就会执行。
如果遍历过程中有子view的dispatchTouchEvent()方法返回true,则将该子view赋值给mMotionTarget,代表找到了一个响应ACTION_DOWN事件的子view对象,然后直接返回true,表示此次事件被响应了。
于是,Activity的dispatchTouchEvent方法也就返回true,而Activity的onTouchEvent方法就不会得到执行。
紧接着,在接收到ACTION_MOVE和ACTION_UP事件时,在第35行将mMotionTarget赋值给target后直接进入到第46行的判断:
如果允许拦截并且拦截了ACTION_MOVE和ACTION_UP事件,则将ACTION_CANCEL事件分发给target,也就是之前响应ACTION_DOWN事件的子view对象,然后直接返回true,表示已经响应了该次事件。
如果ACTION_MOVE和ACTION_UP事件没有被拦截,则直接派发给target,在target的dispatchTouchEvent()方法中进行处理,并根据其返回值来决定Activity的onTouchEvent方法要不要执行。
ViewGroup的dispatchTouchEvent()方法是android事件分发机制中一个很重要的方法,我们分成两条线路来进行分析:
第一条、是我们上边已经分析过的——从Activity的dispatchTouchEvent方法到DecorView的dispatchTouchEvent()方法
第二条、我们着重分析ViewGroup的dispatchTouchEvent()方法中遍历的过程
就像文章开头的图片所展示的一样,一般来说,android中的布局都为 ViewGroup嵌套ViewGroup和ViewGroup嵌套View两种形式,而根据上边的分析我们知道,在研究android事件传递机制时,更重要的是在一个嵌套的布局中是否有clickable或是longclickable的视图存在。
就用文章开头的图片,A、B、C都为ViewGroup对象,假设D为View对象,
下边来分析D在响应和未响应事件两种情况下A、B、C、D之间的事件传递:
当我们点击了有手势标注的地方,B 的dispatchTouchEvent()方法得到执行(从DecorView到A就不分析了,道理一样的)
上图中,最左边部分的序号和代码与上文中ViewGroup的dispatchTouchEvent()方法的源码所对应,红色、蓝色和紫色箭头分别代表DOWN、MOVE和UP事件的处理流程,关于方法的执行顺序,上图标注得比较清楚了,首先DOWN事件派发到B,如果不拦截,则走C的dispatchTouchEvent()方法,如果C也不拦截,则走D的dispatchTouchEvent()方法(我们假设D为一个View对象,如果D是ViewGroup的话,在没有子view的情况下,会走到第39行,执行其父类View的dispatchTouchEvent()方法),
如果D的dispatchTouchEvent()方法返回true,则C的dispatchTouchEvent()方法也返回true,进而B的dispatchTouchEvent()方法也返回true ... ...
如果D的dispatchTouchEvent()方法返回false,那么C的dispatchTouchEvent()方法不会执行第27行,而是走第39行,执行其父类View的dispatchTouchEvent()方法,
如果返回true,则C的dispatchTouchEvent()方法返回true,B的dispatchTouchEvent()方法也就返回true ... ...
如果返回false,则C的dispatchTouchEvent()方法返回false,B的dispatchTouchEvent()方法不会执行第27行,而是走第39行,执行其父类View的dispatchTouchEvent()方法 ... ... 以此类推
在上述过程中,如果D的dispatchTouchEvent()方法返回false,表示DOWN事件没有被响应,则不会再接收到后续的MOVE和UP事件。
如果D的dispatchTouchEvent()方法返回true,表示DOWN事件被D响应了,则在C的dispatchTouchEvent()方法的第26行将D赋值给C的变量mMotionTarget,在B的dispatchTouchEvent()方法的第26行将C赋值给B的变量mMotionTarget ... ... ,当MOVE和UP事件派发到B时,如果在第46行不被拦截的话,则在第61行将MOVE和UP事件派发给target — C处理,当然,如果C不拦截的话,又会在第61行将MOVE和UP事件派发给target — D处理 ... ...
从上文的分析可以看出,android事件分发机制的大致逻辑是:
当屏幕上接收到触屏事件后,不着急处理,
先从应用程序的根view开始遍历,将触屏事件分发给所有包含触屏点坐标的子view(代码的11到32行,当然这个过程中上层的view可以进行拦截,相关分析本文略过了),优先让最里层的子view来处理,如果最里层的子view不处理,再向外层抛,在这个过程中如果有哪一层做出了响应,则代表这次事件被消费了。
<span style="font-family:Microsoft YaHei;font-size:14px;background-color: rgb(255, 255, 255);">public boolean dispatchTouchEvent(MotionEvent ev) {
// 这里是ACTION_DOWN的处理逻辑
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// 每次ACTION_DOWN时,都将mMotionTarget 设为null
// mMotionTarget 是一个比较重要的变量,它不为null则表示找到了响应事件的view对象
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't intercept
// 如果不允许拦截或者允许拦截但不拦截,则执行下边的逻辑
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// We know we want to dispatch the event down, find a child who can handle it, start with the front-most child.
// 将down事件分发下去,遍历,找到一个可以处理事件的子view
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
// 子view必须要是VISIBLE的或者正在执行动画才可以响应事件
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// 如果子view包含触摸点的坐标
if (frame.contains(scrolledXInt, scrolledYInt)) {
if (child.dispatchTouchEvent(ev)) {
// 调用子view的dispatchTouchEvent方法,如果返回true,则将child赋值给mMotionTarget
// 代表找到了一个响应事件的view对象,然后直接返回true
mMotionTarget = child;
return true;
}
// 如果执行到这里,说明ACTION_DOWN事件还没有被响应
}
}
}
}
}
final View target = mMotionTarget;
if (target == null) {
// target == null,意味着没有找到能响应事件的子view,则调用ViewGroup父类View的dispatchTouchEvent方法
return super.dispatchTouchEvent(ev);
}
// 无论 target 是否为 null ,ACTION_DOWN事件的处理都不能走到这里,在之下都是ACTION_MOVE和ACTION_UP的逻辑
// 如果执行到这里,说明有响应ACTION_DOWN事件的view对象,这就看我们是否被允许拦截和要不要拦截了
// 如果允许拦截并且拦截了ACTION_MOVE和ACTION_UP事件,则将ACTION_CANCEL事件分发给target
// 然后直接返回true,表示已经响应了该次事件
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_CANCEL);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do but they should have.
}
return true;
}
// 如果没有拦截ACTION_MOVE和ACTION_UP事件,则直接派发给target
return target.dispatchTouchEvent(ev);
}</span>