【Android】事件分发机制
一、事件分发机制过程
Android事件分发机制是Android开发必须掌握的东西,分发的事件是点击Touch事件,在Android中对应的是MotionEvent对象。
该对象类型主要有三种:
值 | 含义 |
---|---|
MotionEvent.ACTION_DOWN | 按下View。 |
MotionEvent.ACTION_UP | 松开View。 |
MotionEvent.ACTION_MOVE | 移动View。 |
整个过程会形成一个事件列:从用户点击View开始传递MotionEvent.ACTION_DOWN事件,然后随着用户移动,会传递N多个MotionEvent.ACTION_MOVE事件,最后用户松开手指,传递MotionEvent.ACTION_UP事件。
整个事件传递的过程就是一个事件分发的过程。这个过程参与的对象主要有Activity->Window->ViewGroup->View。
二、事件分发机制三个重要方法
事件分发机制的三个重要方法如下:
方法 | 作用 |
---|---|
dispatchTouchEvent() | 分发事件。 |
onInterceptTouchEvent() | 判断是否进行事件拦截。 |
onTouchEvent() | 点击事件处理。 |
任玉刚大神的《Android开发艺术探索》里面有一段伪代码表达的非常清楚~
public boolean diapatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对于传入的事件,首先调用diapatchTouchEvent方法开始进行分发,然后调用onInterceptTouchEvent来判断是否进行拦截,如果要拦截,那么事件就在这个ViewGroup进行处理了,onTouchEvent会被调用。如果不拦截事件,就会继续传递给子View的dispatchTouchEvent进行处理。层层传递,直到事件被拦截。
三、源码
源码基于Android7.1。
1、从Activity出发。里面的dispatchTouchEvent方法如下。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看见调用了getWindow().superDispatchTouchEvent(ev)方法,如果这个方法返回true,就直接返回,即消费了,否则Activity会调用onTouchEvent方法。
2、先看Window的处理,getWindow获取对应的Window,进入Window.java找对应的方法。可以看见是一个抽象的方法。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
具体实现在哪呢?可以看见前面的说类说明有这么一段话。
The only existing implementation of this abstract class is android.view.PhoneWindow
所以具体实现类是PhoneWindow,进入PhoneWindow.java,看superDispatchTouchEvent方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看见直接调用了mDecor的superDispatchTouchEvent方法,即Windows其实对事件没有进行任何处理。
mDecor是什么呢?
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
下面有一张图其实比较清楚的能展示DecorView。
DecorView.png可以看见DecorView其实就是我们的顶层View,它是一个FrameLayout布局,从源码也可以看见它是继承自FrameLayout。里面是一个线性布局,包含一个TitleBar一个content,content就是我们每次在setContent的内容,即我们自定义的布局。
public class DecorView extends FrameLayout
说到这里呢,再返回去看superDispatchTouchEvent函数,这个调用了super的dispatchTouchEvent方法,一直往父类看,可以看见super的dispatchTouchEvent方法在FrameLayout没有实现,再往上是ViewGroup类,这里面就有实现了,所以DecorView其实直接调用了ViewGroup的dispatchTouchEvent方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
3、看ViewGroup的dispatchTouchEvent方法。
可以说代码巨长无比,但是总体思路和前面的伪代码一样,我们挑重点。
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
intercepted是来标记是否在这个地方进行拦截,先不管最外面的else,先从最里面看起,最里面调用了onInterceptTouchEvent来判断是否拦截,即伪代码里面的内容。
往外有个判断disallowIntercept,如果mGroupFlags有FLAG_DISALLOW_INTERCEPT这个标记的话,直接就不拦截,FLAG_DISALLOW_INTERCEPT这个是什么呢?这个其实是在子View里面设置的,即不允许父容器拦截,设置这个标记之后,父容器就不进行拦截。
但是往前还有这么一段话。
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
即当MotionEvent.ACTION_DOWN事件的时候,会执行resetTouchState函数,这个函数里面有个处理就是去掉这个标记。所以开发中有时候遇到requestDisallowInterceptTouchEvent设置失效,就是在这里失效的。
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
再往外有一个mFirstTouchTarget这个标记,这个标记是啥呢?mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功处理,mFirstTouchTarget就会指向该元素。
如果onInterceptTouchEvent()返回true,说明ViewGroup拦截事件,mFirstTouchTarget为null,同一序列的事件都由它处理,onInterceptTouchEvent也不会再调用了,因为actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null条件都不满足。
最后看看onInterceptTouchEvent方法,可以看见,默认是不拦截的。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
分析到这里,先得出一点小结论:
- 当ViewGroup开始拦截事件之后,后面的事件列都会交给它,且不调用onInterceptTouchEvent方法。
- 子元素可以设置FLAG_DISALLOW_INTERCEPT标记,这样父元素ViewGroup就不会进行拦截,但是有个前提就是父元素一开始不拦截MotionEvent.ACTION_DOWN。
- 只有diapatchTouchEvent一定每次调用,onInterceptTouchEvent不一定每次调用。
- ViewGroup的onInterceptTouchEvent方法默认是不拦截的。
继续,如果不拦截的话会干嘛呢?即在if (!canceled && !intercepted)这个if条件里面,直接看里面关键部分。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
}
这里是遍历所有的子View。之后怎么处理呢,往下看。接下来有几个判断。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
canViewReceivePointerEvents函数。从代码可以看出这个是在判断View可见并且没有播放动画才可以接收。
/**
* Returns true if a child view can receive pointer events.
* @hide
*/
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
isTransformedTouchPointInView函数。从代码可以看出必须要点在View内才可以,这里是用View的Top和Left参数判断,即View的真身。
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
/**
* @hide
*/
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
综上,遍历之后判断子元素View是否可以接收事件的条件有三个:
- View可见
- View没有在播放动画
- 点击的点的坐标在View里面
然后,如果可以接收,继续执行。
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;
}
接着一句句看上面的部分,首先是dispatchTransformedTouchEvent函数。直接看关键代码,一般关键代码都在后面。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
可以看见,如果子View不为null,则调用child.dispatchTouchEvent(transformedEvent)将事件分发下去。如果子元素处理了,那么dispatchTransformedTouchEvent会返回true。返回true之后会调用执行newTouchTarget = addTouchTarget(child, idBitsToAssign),这里就是对mFirstTouchTarget 进行了赋值,前面讲的mFirstTouchTarget 就是在这里赋值的。
如果在遍历完子View以后ViewGroup仍然没有找到事件处理者即ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件。即dispatchTransformedTouchEvent返回了false,那么mFirstTouchTarget 就为null。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
这时候还是调用dispatchTransformedTouchEvent,只是有一个不同是此时第三个参数也就是child传入了null。再贴一次代码,可以看见此时调用super.dispatchTouchEvent(transformedEvent),即调用父类View的dispatchTouchEvent方法。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
4、此时看View.java的dispatchTouchEvent方法。
看最关键的几句话。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
如果View设置了OnTouchListener且这个监听里面的onTouch返回true,那么onTouchEvent就不被调用。反之,调用自身的onTouchEvent方法。
接下来看onTouchEvent方法。这个方法比较长。由一个个判断来看。
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);
}
首先,如果View被设置为disable,那么只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件。View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
...
}
接下来,即View为enable的时候,只要CLICKABLE和LONG_CLICKABLE有一个为true,也会消费这个事件。里面是一些列的action判断,这里主要看ACTION_UP,执行了一个performClick()方法。这个方法即如果设置了mOnClickListener监听,那么就会执行对应的监听里面的onClick方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
从这里可以总结出优先级OnTouchListener>OnTouchEvent>OnClickListener,当设置了OnTouchListener且OnTouch方法返回true的时候,就不再执行后面两个。
同时可以看见,如果View全部不进行消费,那么事件又会一层层回传,直到Activity那儿执行onTouchEvent方法。回到Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
看下对应的onTouchEvent方法,这里判断了一个shouldCloseOnTouch方法,这个其实只是判断是否点在了空白区域,所以点击空白区域会关闭就是因为这里执行了finish()。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
四、太乱了,总结一下
所有入口都是dispatchTouchEvent,从Activity->Window->ViewGroup->View依次传入,如果onInterceptTouchEvent为true(不一定都执行)拦截,否则继续一级级往下传。事件处理onTouchEvent为true则消费掉,否则原路返回一级级往上传。
任玉刚大神的《Android开发艺术探索》给出了11点结论帮助理解。
1、同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
2、正常情况下,一个事件序列只能被一个View拦截且消耗。因为前面源码分析过了,当一个事件交给一个View执行之后,就不再执行onInterceptTouchEvent进行判断了。但是通过特殊手段可以使得事件列里面不同事件被不同View处理,比如一个View本该处理然后通过onTouchEvent强行转给其他View。
3、某一个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递到它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个时间序列内的其他方法都交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截啦。参考2。
4、当某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,两者类似。
5、如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失(当然了:点击事件需要消耗,DOWN 和 UP事件的),此时父元素的onTouchEvent并不会被调用,并且当前的View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
6、ViewGroup默认不拦截任何事件。Android源码中的ViewGroup的onInterceptTouchEvent方法默认不拦截false。
7、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
8、View的onTouchEvnet默认都会消耗(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分请情况,比如Button的clickable属性默认为true,而TextView 的 clickable属性默认为false。
9、View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或则longClickable有一个为true,那么它的onTouchEvent就返回true。
10、onClick会发生的前提是当前View是可点击的,并且他收到了down和up的事件。
11、事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。