一、概述
一直以来都想把Android中事件分发机制原理好好理解一下,但是一直没有时间,刚好趁着这次在家办公的机会把其中的源码好好的梳理了一下。一般来说一个完整事件会包含ACTION_DOWN、ACTION_MOVE和ACTION_UP三种事件,其中ACTION_DOWN事件的出现说明是一个新的事件发生,ACTION_MOVE事件是用户手指在屏幕上移动,ACTION_UP则是抬起手指,也说明该事件已经接近了尾声。当然除了这三种事件以外还存在ACTION_CANCEL,比如手指滑动到了屏幕边缘部分就会产生该事件。
事件从屏幕上产生然后进行传递分发大致经历了如下几个类:屏幕->Activity->PhoneWindow->DecorView->ViewGroup->View。其中事件的整个分发过程则涉及到了如下三个重要的方法:
(1)dispatchTouchEvent();在ViewGroup的该方法中主要是用于将事件向子View进行分发,如果没有子View处理事件或者无子View,那么当前ViewGroup会处理该事件。
(2)onInterceptTouchEvent();该方法只存在于ViewGroup中,主要用于在ViewGroup拦截事件。因为View中不会存在子View,所以不需要拦截事件就能够处理事件。
(3)onTouchEvent();处理事件的具体函数,在该函数的ACTION_UP事件处理过程中会执行到用户注册的点击监听器中,也就是click方法。
上述三个方法的大致逻辑可以用如下伪码来进行表示:
void dispatchTouchEvent(event):
if(onInterceptTouchEvent()):
onTouchEvent()
else:
child.dispatchTouchEvent();
也就是说如果当前ViewGoup不拦截事件,则会将该事件分发给具体的子View进行执行,当然这只是一个其中非常简单的事件处理逻辑而已。除了上述的情况之外还存在:
(1)如果没有子View处理事件,事件是如何处理的?
(2)如果某一个子View处理了某一个事件的ACTION_DOWN事件,那么后续的事件又是如何直接传递给它的?等等。
上面的一系列问题都会在源码解析的过程中一步步的被剥开。不过在讲解具体的源码之前还需要了解一下事件在Android中呈现方式,这又利于后续源码的理解。在Android中用一个32位的整数来表示一个TouchEvent事件,前16位暂时不用;后16位的低8位表示Touch的具体动作(down、up、move等),高8位用于表示Touch事件中多点触控的索引值。在平时开发中我们可以通过如下三个方法来获取事件类型或者多点触控索引值:
(1)getAction():在单点触控中,我们可以直接通过该方法获取到事件类型,但是多点触控中其中还包含了多点触控的索引值。我们可以通过位运算的方式获取具体的事件类型;
(2)getAcionMasked():用于获取具体的事件类型;
(3)getActionIndex():用于获取多点触控的索引值。
二、源码分析
1、概述
在Android framework中,事件从屏幕产生之后首先会传递到Activity中,然后会传递给PhoneWindow进行处理,再然后会传递到DecorView中并由该ViewGroup传递给父ViewGroup进行拦截、分发最后再处理,如果所有的View都不处理该事件,那么该事件最终会被Activity给消化掉,如果Activity也不处理该事件,那么该事件则会直接消失。因此,我们可以从Activity开始一步步的对事件的整个处理过程进行解析。
2、Activity事件处理
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//官方解释是说,当当前Activity处于前台状态,如果你想要知道用户正在以某种方式和设备进行交互,那么就可以实现该方法
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在Activity并没有对事件做任何处理只是将该事件转发给了window对象,当然在这里的window对象就是PhoneWindow的实例(在Activity初始化数据的时候也就是attach方法中可以知道)。
3、PhoneWindow事件处理
我们知道在Activity中的window对象就是PhoneWindow因此我们直接看PhoneWindow中的事件处理过程。
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
该方法更简单,直接将事件转发给了DecorView进行处理,因此我们直接转到DecorView中。
4、DecorView事件处理
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
这里为什么会存在Window.Callback的处理方式不是特别清楚,先搁置着后续再了解。如果不存在Window.Callback那么该事件则会交给DecorView的父ViewGroup进行处理,而DecorView的父类是FrameLayout,FrameLayout又继承了ViewGroup,并且看源码会发现FrameLayout并没有重写dispatchTouchEvent方法。所以事件也就理所当然的被传递到了ViewGroup的dispatchTouchEvent方法中了,也就是我们的重头戏来了(只讲解单指触控)。
5、ViewGroup事件处理逻辑
对于该类中的dispatchTouchEvent方法可以大致划分为三个部分:(1)是否拦截事件处理判断;(2)寻找可以处理事件的子View,如果存在该子View那么将事件分发给该子View并停止查找;(3)如果不存在子View处理事件,则当前ViewGroup消费事件,否则将后续事件全部发送给该子View进行处理。我们挑选这几个部分的核心源码进行讲解。
5.1、判断是否事件拦截
在这之前还会判断当前window是否被遮挡,如果被遮挡了那么直接不处理该事件;然后会判断当前事件是否是ACTION_DOWN事件,如果是那么会重新初始化所有相关的状态值。然后判断当前ViewGroup是否拦截事件,源码如下:
final boolean intercepted;
//如果当前是ACTION_DOWN事件或者上一次的down事件存在子View处理,则判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//获取当前ViewGroup是否允许拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//判断当前ViewGroup是否拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
5.2、将事件分发给子View
在事件的分发过程中还涉及到了多点触控的判断,因为这里只是对事件的分发流程进行梳理,所以多点触控相关的就直接略过了.......
//如果当前不是取消事件并且当前ViewGroup没有拦截事件则判断是否将事件传递给子View
if (!canceled && !intercepted) {
//是否将该事件分发给指定的View
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//在ACTION_DOWN事件到来时会分发事件给子View
if (actionMasked == MotionEvent.ACTION_DOWN
//多指触控
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
//鼠标移动
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.......
final int childrenCount = mChildrenCount;
//从后向前遍历子View列表,查找可以处理该事件的View
if (newTouchTarget == null && childrenCount != 0) {
...........
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//查找可以处理该事件的指定View
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果当前View可见且没有播放动画以及事件的坐标落在该View坐标范围内。 //上述两个条件如果有一个不满足则舍弃当前View,继续遍历下一个
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...............
//将事件分发给当前子View,如果返回true表示当前子View消费该事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
...........
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY(); //将当前子View包装为TouchTarget对象并让mFirstTouchTarget变量指向该对象
//用于记录处理该事件的子View
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//已经有子View处理该事件,跳出循环结束查找
break;
}
//该事件已经有子View进行处理,所以标识该事件能够被特定的子View进行处理
ev.setTargetAccessibilityFocus(false);
}
.......
}
................
}
}
在该部分中,会从后向前遍历所有的满足条件的子View,如果当前事件是指定给特定的View进行处理,则会查找该特定的子View,否则会查找第一个满足如下两个条件的子View进行处理(这也是查找满足接受事件子View的硬性条件):
(1)可见并且没有播放动画;
(2)当前事件点的坐标在当前子View的范围内;
如果满足上面两个条件的View处理了该事件,那么会将后续的move和up事件也分发给该View进行处理,并且停止查找;否则,继续查找满足条件的子View处理该事件。当查找到了满足条件的子View之后会通过方法dispatchTransformedTouchEvent进行传递,该方法后续进行讲解。
5.3、没有子View处理事件或者将后续事件转发给同一个子View
该部分的主要做了如下判断,如果当前事件没有子View处理,或者是当前ViewGroup拦截当前事件,那么该事件就会由当前ViewGroup进行消费。反之,当前事件的后续事件则会继续发送给处理了当前ACTION_DOWN事件的子View进行处理(简单来说就是一个事件会由同一个View进行消费)。当然这部分也存在多点触控相关的逻辑,暂时略过。
//没有子View处理事件或者当前ViewGroup拦截了事件,那么当前ViewGroup处理事件
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//当前是ACTION_DOWN事件并且存在子View处理该事件
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//判断是否取消事件分发给子View
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
//将接下来的事件分发给上一次接收该ACTION_DOWN事件的子View
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
事件的最终分发都放到了dispatchTransformedTouchEvent方法中,我们来看看该方法。
5.4、dispatchTransformedTouchEvent
在该方法中,首先判断是否需要取消事件,然后判断是否存在子View处理该事件,如果存在则将事件分发给具体的子View;如果不存在则当前ViewGroup处理该事件。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//取消事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//如果存在子View处理事件则传递给具体的子View 进行处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//获取是哪一个触控点触发的事件(或者说是手指)
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
//如果不存在子View处理该事件,那么当前ViewGroup处理该事件
//如果存在子View处理该事件,则首先调整事件发生坐标然后将该事件分发给子View处理
//返回处理结果
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
return handled;
}
当目前为止ViewGroup的事件分发基本也就如此了,其实将其中的代码分成一个一个小的部分来看并不复杂。回顾一下当事件进入到ViewGroup的dispatchTouchEvent中的时候,做了哪些操作:
(1)判断当前View所在window是否被其它window所遮挡,如果被遮挡那么当前ViewGroup没权处理该事件;
(2)如果是ACTION_DOWN事件,首先会清除之前的所有状态;
(3)判断当前ViewGroup是否拦截当前事件;
(4)如果不拦截当前事件并且当前事件也不取消,则从后向前遍历ViewGroup中所有的子View,并查找到第一个满足条件的子View处理该事件;
(5)如果存在子View处理该事件则结束查找;如果没有子View处理当前事件,那么当前ViewGroup则会处理该事件;
(6)将ACTION_DOWN后续的事件都分发给同一个子View进行处理。如果当前ViewGroup拦截了后续的ACTION_MOVE、ACTION_UP事件,那么该事件会被取消,并且当前ViewGroup也不会处理该事件。
在dispatchTransformedTouchEvent方法中,不论是ViewGroup处理事件还是子View处理事件,最终都会调用到View中的dispatchTouchEvent方法中,也就是事件的最终处理都是在View中进行,所以接下里我们对View中的事件处理逻辑进行梳理。
6、View事件处理逻辑
阅读View中的dispatchTouchEvent方法发现代码并不是很多,逻辑也并不复杂。其中当前我们所关心的就是如下代码部分:
//用户如果设置了onTouchEvent相关的listener,则将该事件传递给该listener进行处理
//否则传递给当前View的onTouchEvent方法进行处理
public boolean dispatchTouchEvent(MotionEvent event) {
.............
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
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的onTouchEvent方法里面,如果对于事件相关我们有自己的逻辑需要处理,则可以通过重写onTouchEvent方法来实现自己想要实现的逻辑了。我们来看看onTouchEvent中的部分代码。
public boolean onTouchEvent(MotionEvent event) {
...........
//如果当前Viwe不可用,但是可点击,那么该View仍然可以消费该事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
............
//如果当前View可点击或者该View是tooltip类型,那么就会消费该事件
//那么接下来就是对ACTION_DOWN ACTION_MOVE ACTION_UP以及ACTION_CANCEL几个事件分别进行处理了
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
................
return true;
}
return false;
}
这里我们省略了onTouchEvent中对ACTION_DOWN ACTION_MOVE ACTION_UP以及ACTION_CANCEL几个事件分别处理的细节,有需要的可以百度一下,一大把的讲解。不过这里还是要提一下在对ACTION_UP事件处理的最后会通过调用performClickInternal方法调用到performClick方法中,最后在该方法中调用用户设置的点击事件监听器。所以对于用户设置的listener中的onTouchEvent方法、View 中的onTouchEvent方法以及设置的click事件优先级从高到低。如果用户设置了onTouch相关的监听器,那么后续两个方法也就不会执行了。
四、总结
Android事件大致的分发过程以及处理逻辑基本上来说从Activity到具体的View是基本讲完了,讲解的整个过程中下面几个问题也算是包含在了里面:
(1)Android事件分发过程中所涉及到了几个类;
(2)ViewGroup拦截事件之后事件处理逻辑;
(3)如何查找满足处理事件条件的子View,以及如何将事件分发给子View;
(4)如果一个子View接收了某一个事件到ACTION_DOWN事件之后,那么该事件的后续事件又是如何直接分发给它的;
(5)如果没有查找到能够处理事件到子View,那么事件又是如何处理到等等。