所谓的点击事件分发过程,其实就是当我们点击屏幕,产生了一个MotionEvent之后,系统将这个事件传递给一个具体View的过程。总的来说,事件总是先传递给Activity,然后传递给Window,再传递给顶级View(Activity→Window→DecorView),最后再按照事件分发机制一层一层向下去分发事件。而这个分发过程由三个很重要的方法来共同完成:
上述三个方法之间的关系可以通过如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){//如果拦截
consume = onTouchEvent(ev);
} else {//如果不拦截
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过上面的伪代码,我们可以大致了解点击事件的传递规则:对一个ViewGroup来说,当一个点击事件传递给它时,它的 dispatchTouchEvent 方法会被调用,如果这个ViewGroup拦截此事件,那么事件将会交由该ViewGroup来处理(即调用 onTouchEvent 方法);如果它不拦截此事件,那么事件将会向下传递给它的子元素,接着子元素的 dispatchTouchEvent 方法会被调用,如此反复直到时间被最终处理。
关于事件传递机制的一些结论:
1.点击事件向上传递至顶级View
本文最开始有提到,事件总是先传递给Activity,然后传递给Window,再传递给顶级View(Activity→Window→DecorView),最后再按照事件分发机制一层一层向下去分发事件。所以事件最开始是传递给当前Activity,由 Activity 的 dispatchTouchEvent 方法进行事件派发,如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到,Activity拿到事件之后会交给它所属的 Window 进行分发,如果返回true,那么整个事件循环就结束了,如果返回false,意味着所有 View 的 onTouchEvent 返回了false,最后 Activity 的 onTouchEvent 会被调用(上述结论中的第4条)。
接下来我们看下Window是如何处理事件的。值得注意的是 Window 是一个抽象类,其 superDispatchTouchEvent 方法也是一个抽象方法,因此我们需要查看 Window 的具体实现类的源码,即 PhoneWindow 的 superDispatchTouchEvent 方法,如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可见 PhoneWindow 将事件直接传递给了 DecorView。从这里开始,事件就已经传递到顶级View了,也叫根View,通常而言顶级View都是ViewGroup。
2.顶级View对点击事件的分发过程
在上一节中,有说事件分发机制涉及到三个重要的方法,并且对于ViewGroup来说,如果它拦截该点击事件,那么事件就会交由它来处理,如果它不拦截该事件,那么则会交由它的子元素处理。我们先来看下 ViewGroup 的 dispatchTouchEvent 方法中有关是否拦截该事件的逻辑代码块:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {//DOWN事件总会进一步判断是否拦截,除此之外的其他事件需要看mFirstTouchTarget是否为空
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 {//如果不是DOWN事件,并且mFirstTouchTarget为空
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;//拦截事件
}
上面注释得比较清楚了,需要说明的有两点。第一点是 mFirstTouchTarget 是一个指针,指向处理了该事件(DOWN事件)的子元素,换言之如果该 ViewGroup 的所有子元素都没有处理该事件,那么 mFirstTouchTarget == null。这样就会使同一个事件序列中除DOWN以外的其他事件全部被 ViewGroup 拦截。这也就说明了上一节中的第4条结论。第二点是有关子元素请求父元素禁止拦截的,其方法为 requestDisallowInterceptTouchEvent。一旦子元素调用了 requestDisallowInterceptTouchEvent 方法,那么父元素的对应flag就会被置高,ViewGroup 将无法拦截除了DOWN以外的其他事件。这是因为ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置上述flag。如下:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
通过上述代码块的分析,就可以得出上一节中得出的第2条结论,以及第11条结论。
接着再看当ViewGroup不拦截此次事件时,如何分发给子元素处理,如下:
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);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果子元素可以接收到点击事件(通过了上述判断)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//判断条件内部调用child.dispatchTouchEvent方法
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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);//方法内部完成了mFirstTouchTarget的赋值
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
上述代码首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件(判断条件:1.是否在播放动画 2.点击事件坐标是否落在子元素内),如果子元素可以接收到事件,那么将会交由它来处理,具体是在dispatchTransformedTouchEvent 方法中调用的,如下(注意此时传入的第三个参数child非空):
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent 方法返回 true,那么 mFirstTouchTarget 就会被赋值并跳出 for 循环;如果子元素的 dispatchTouchEvent 方法返回 false,ViewGroup 就会把事件分发给下一个子元素。如果遍历所有子元素后事件都没有被合适处理,那么ViewGroup就会自己处理该事件,如下(注意此时传入的第三个参数为null):
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
3.普通View对点击事件的处理过程
老样子,先看它的 dispatchTouchEvent 方法,如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//如果有设置onTouchListener并且onTouch返回true
result = true;
}
if (!result && onTouchEvent(event)) {//注意:如果此时result为true,onTouchEvent不执行
result = true;
}
}
......
return result;
}
通过上述代码,可以看到,View对点击事件的处理首先会判断有没有设置onTouchListener,如果onTouchListener中的onTouch 方法返回 true,那么 onTouchEvent 就不会被调用,可见 onTouchListener 的优先级高于 onTouchEvent,这样的好处就是方便在外界处理点击事件。
接着再分析 onTouchEvent 的实现。先来看下上一节中的第9条结论是否正确,如下:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
不难看出,即使View是 disable 的,其返回值clickable只与三种点击状态有关,需要同时为false,才返回false。
再看一下 onTouchEvent 中对点击事件的具体处理,如下:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
}
mIgnoreNextUpEvent = false;
break;
......
}
return true;
}
从上述代码来看,只要 View 不是不可点击的非提示框控件,就会进入上面的 if 判断,最后默认返回 true,消耗此次事件。这就证实了上一节中的第8条结论。然后就是第10条结论,当ACTION_UP事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener,那么 performClick 方法内部会调用它的 onClick 方法。