Android学习笔记之事件分发机制完全解析
事件分发机制,作为Android中必须要掌握的重点,同时也是难点,想要解决开发中的一些问题,必须对它的整个流程有着较好的掌握。这几天阅读了《Android开发艺术探索》等书籍,结合自己的理解,记录下自己的所学,这里只是初步学习总结,想要完全地搞明白事件分发,还需要进一步学习。文中如果有错误,欢迎大家指出。
一、引入
想象生活中的一个例子,老板,项目经理,程序员。老板接到一个任务,分配给项目经理完成,项目经理又把任务分给程序员。程序员完成任务,告诉项目经理任务完成了,项目经理再向老板报告任务完成了。从老板接到任务,到老板最终去交付任务,这是个完整的过程。
但是在这个过程中,会有其它情况。假如在一开始老板接到任务时,决定自己完成,不需要把任务再分配,那么老板就自己做,项目经理和程序员就没事。同样,如果项目经理决定自己去做,那么就没有程序员的事。
这个例子大家肯定都能听懂,下面我们来看Android中的事件分发。
二、事件分发机制概述
1、概念
我们知道,Android的界面可能是由多个视图层层嵌套构成,当一个触摸事件发生时,系统需要把这个事件传递给一个具体的View,由它来完成处理。从事件发生,到传递给具体的View去完成,这个传递的过程就是View 事件分发。
2、相关
触摸事件,简单来说就是是触摸屏幕产生的动作事件,比如常见的手指按下,移动,抬起等等,Android为我们提供了一个专门的MotionEvent类,它包含了发生的动作事件以及相关坐标信息,利用MotionEvent,我们可以处理很多与动作相关的工作。
这里的View其实指的是View以及ViewGroup,我们知道View是Android中所有控件的基类,而ViewGroup翻译为视图组,它是继承自View的,可以包含子控件。我们在接下来的讨论中,会把ViewGroup和View分开讨论。
3、详解
事件分发过程主要涉及到三个重要方法,主要介绍如下:
dispatchTouchEvent 用于事件分发
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 的事件分发
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。判断是否拦截时,会看disallowIntercept,默认是false不允许拦截,所以!disallowIntercept为true,然后调用onInterceptTouchEvent为false,即不拦截。有种情况,如果ACTION_DOWN判断时被ViewGroup拦截,那么mFirstTouchTarget != null就不成立,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP来临时,不进行判断,直接拦截。
得出两条结论,某个View一旦决定拦截一个事件后,那么系统会把同一个事件序列的其它方法都交给这个View处理。某个View如果不消耗ACTION_DOWN事件交给了子View处理,那么同一个事件序列的其它方法都不会交给它处理。
2、当ViewGroup不拦截事件,事件分发给子View处理。
//子Viewfinal 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就自己处理事件。
四、View 的事件分发
除了ViewGroup之外的View进行事件分发时,因为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,那么它就会消耗事件。
五、总结
事件分发机制的初步学习就到这里,了解了这些原理,对于遇到的常见的滑动冲突问题应该都能解决了,之后还要更进一步的了解事件分发,做到了然于胸。
- 赞