事件分发机制,是Android提供的一套完善的对触摸事件进行处理的机制,熟悉整个事件分发流程很有必要,因为它也是Android中常见的滑动冲突问题解决的理论基础。这几天阅读了《Android开发艺术探索》等书籍,总结如下。
一、引入
二、事件分发机制
1.概述
2.详细
三、源码解析
1.ViewGroup事件分发
2.View事件分发
四、滑动冲突解决
五、总结
一、引入
在介绍Android事件分发机制之前,我们先看生活中的一个例子。公司里有三个角色,老板,项目经理,程序员。有一天老板接到一个任务,他将任务分配给项目经理完成,项目经理又把任务分给程序员。程序员完成任务后,告诉项目经理任务完成了,项目经理再向老板报告任务完成了。从老板接到任务,到老板最终去交付任务,这是个完整的过程。
在这个过程中,可能会有其它情况。假如在一开始老板接到任务时,决定自己完成,不需要把任务往下分配,那么老板就自己做,项目经理和程序员就没事。同样,如果项目经理决定自己去做,那么就没有程序员的事。上面的这个例子其实就是任务在老板、项目经理和程序员这三个角色间的传递过程,Android中屏幕上的触摸事件就相当于这个任务,事件分发就类似于这个传递过程。
二、事件分发机制
我们知道,Android的界面可能是由多个视图层层嵌套构成,一个ViewGroup视图组合中可以包含其它的ViewGroup以及View,当一个触摸事件发生时,系统需要把这个事件传递给一个具体的View,由它来完成处理。从事件发生,到传递给具体的View去完成,这个传递的过程就是View的事件分发。
概述
在事件分发机制中,涉及到的几个关键部分分别是:TouchEvent(触摸事件)、ViewGroup(视图组合)、View(视图)。下面先对这几个部分做个介绍。
- TouchEvent(触摸事件)
触摸事件就是触摸屏幕产生的动作事件,比如常见的手指按下,移动,抬起等等,Android为我们提供了一个专门的MotionEvent类,它包含了发生的动作事件以及相关坐标信息,利用MotionEvent,我们可以处理很多与动作相关的工作。
- View
我们经常提到View事件分发机制,其实这里指的是View以及ViewGroup,我们知道View是Android中所有控件的基类,而ViewGroup翻译为视图组合,它是继承自View的,可以包含子控件。我们在接下来的讨论中,会把ViewGroup和View分开讨论。
详解
上面介绍了一些事件分发的基本概念,下面对分发流程有个总体的把握。Android中事件分发机制主要涉及到三个重要方法,如下:
- dispatchTouchEvent ( MotionEvent event ) 事件分发
- onInterceptTouchEvent 决定是否拦截事件
- onTouchEvent 处理事件
上面三个方法之间的关系大概如下,当事件传递到某个View时,先执行dispatchTouchEvent方法进行事件分发,在这个方法内会调用方法onInterceptTouchEvent方法来决定是否拦截,如果返回true表示拦截,则调用onTouchEvent进行事件处理,否则继续往下传递,执行子View的dispatchTouchEvent方法。
需要注意一点,View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。ViewGroup默认不拦截任何事件,因为从源码中可以看到ViewGroup的onInterceptTouchEvent方法默认返回false.
我们知道,四大组件中,Activity通常提供界面用于交互,我们会通过setContentView来设置界面布局,一般如果我们不希望布局顶部出现一个标题栏,我们可能会调用requestWindowFeature(Window.FEATURE_NO_TITLE);方法,这里我们简单了解一下Android的界面架构。
界面上一个点击事件发生时,它最先被传递的是给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发,而Activity内部其实是包含一个Window的,这个抽象Window的实现是PhoneWindow,Activity把事件传递给PhoneWindow,PhoneWindow里又包含DecorView,PhoneWindow继续把事件传递给DecorView,DecorWindow里包含有我们设置的布局,DecorView继承自FrameLayout,事件最终传递给我们设置的布局,一般来说设置的布局是一个ViewGroup。所以,触摸事件最后就是在ViewGroup中的分发过程。
三、源码解析
前面我们已经提到,事件分发机制其实是触摸事件在ViewGroup和View两种情况下的分发过程,下面我们结合源码来分析,因为View的过程相对来说较为简单,我们先看ViewGroup事件分发。
ViewGroup事件分发
ViewGroup事件分发过程简述主要如下,事件到达ViewGroup后会调用方法dispatchTouchEvent,在其中会调用onInterceptTouchEvent进行判断是否拦截,如果返回true表示拦截则事件由ViewGroup处理,如果返回false不拦截,则事件会传递给子View,子View的dispatchTouchEvent会被调用。默认情况下,onInterceptTouchEvent返回false.
下面我们看下源码。
1、首先是dispatchTouchEvent方法里判断是否拦截。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//默认是false 允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
}else {
intercepted = true;
}
这里可以看到,ViewGroup会在两种情况下进行是否拦截的判断,第一种是发生ACTION_DOWN事件,第二种是mFirstTouchTarget != null。第二种情况是指,ViewGroup是否不拦截事件并把事件交由子View处理,如果是,那么mFirstTouchTarget != null就成立。
进行判断时,会看变量disallowIntercept的值,这个值默认是false不允许拦截,所以!disallowIntercept为true,然后调用onInterceptTouchEvent为false,即不拦截。有种情况,如果ACTION_DOWN判断时被ViewGroup拦截,那么mFirstTouchTarget!=null就不成立,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP来临时,不进行判断,直接拦截。
这里有两条结论,某个View一旦决定拦截一个事件后,那么系统会把同一个事件序列的其它方法都交给这个View处理。某个View如果不消耗ACTION_DOWN事件交给了子View处理,那么同一个事件序列的其它方法都不会交给它处理。
2、当ViewGroup不拦截事件,事件分发给子View处理。
//子View
final View[] children = mChildren;
//循环遍历
for (int i = childrenCount - 1; i >= 0; i--) {
... ...
//如果子View接收不到事件 或者 不在播动画 就不分发
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//分发事件给子View
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//调用子元素的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
可以看到大概流程如下,循环遍历子View,判断子元素能否接收到点击事件。能否接收到事件主要由两点衡量,一是是否在播放动画,二是点击事件的坐标是否落在子元素的区域内。如果子元素满足条件,则事件传递给子View处理。dispatchTransformedTouchEvent方法里调用了子View的dispatchTouchEvent方法。
如果子View的dispatchTouchEvent返回true,那么终止子元素的遍历,如果返回false,则继续分发给下个子元素。如果遍历所有的子元素后事件都没处理,那么ViewGroup就自己处理事件。
**综上,触摸事件传递到ViewGroup时,会执行方法dispatchTouchEvent()进行事件分发,如果事件是Down类型(或者同一事件序列没被拦截已经交由子元素处理),那么就调用方法onInterceptTouchEvent进行拦截判断,默认情况下不会拦截事件。ViewGroup不拦截的话,那么就会遍历它的子View,判断能否接收到事件,如果接收到那么就调用子View的dispatchTouchEvent方法继续进行分发。如果遍历子View后都没处理事件,那么ViewGroup自己处理事件。
**
View事件分发
View的事件分发比ViewGroup简单,因为View不包含子View,所以它只能自己处理事件。
下面是它的dispatchTouchEvent方法内的部分源码。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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对点击事件的处理,首先会判断有没有设置OnTouchListener,因为OnTouchListener的优先级高于onTouchEvent。
onTouchEvent中,即使View处于不可用状态,照样会消耗点击事件。下面代码可以看出来。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
A disabled view that is clickable still consumes the touch events, it just doesn't respond to them,一个不可用的View仍然可以消耗事件,只是不做任何响应。
onTouchEvent中对点击事件的具体处理流程大概如下,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗事件,返回true。总的来说,View的可不可用不影响是否消耗事件,只要clickable或者longClickable有一个为true,那么它就会消耗事件。
**综上,触摸事件传递到View时,会执行方法dispatchTouchEvent()进行事件分发,这里会判断有没有设置OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不会被调用。View的onTouchEvent默认都会消耗事件,除非它是不可点击的(clickable和longClickable同时为false),而View的enable属性并不影响onTouchEvent的返回值。
**
四、滑动冲突解决
上面主要主要介绍了View的事件分发机制的整个过程,在平常的开发中,在熟悉整个分发过程后,滑动冲突问题应该就不再是难题了。下面主要以一个典型的例子,介绍下滑动冲突问题的解决。
滑动冲突的产生主要是因为界面中内外两层都可以滑动,比如一个界面外部可以左右滑动,内部可以上下滑动。这时就可以采取外部拦截法,前面我们提到分发过程中方法onInterceptTouchEvent主要是用于判断是否拦截,那么外部拦截中我们可以重写父容器的onInterceptTouchEvent方法,根据需要决定是否拦截。
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
五、总结
到这里关于Android中View的事件分发机制就介绍的差不多了,欢迎指正批评。