上一章主要说了View分发的简单流程,下面通过源码来看清楚分发机制
科普:当一个点击事件发生后,最开始的传递过程是这样的:Activity–Window–顶级View,当顶级View收到事件后,就会按分发机制把事件分发下去
当事件传递到顶级View(我们在Activity通过setContentView那个,是个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,有两种情况要判断(有一个为true就调用onInterceptTouchEvent询问自己是否要拦截)
- actionMasked == MotionEvent.ACTION_DOWN:当前点击事件为按下时,ViewGroup总是会问自己是否要拦截事件(调用onInterceptTouchEvent方法,默认不拦截)
- mFirstTouchTarget != null:如果不拦截Down事件,在分发过程中,若有子View成功消费Down事件,则mFirstTouchTarget 指向那个子View
结论:若上面两个条件都为false,即在一个事件序列中ViewGroup拦截Down事件或者没有子View想要处理Down事件,则后面的Move,Up事件来到ViewGroup都会直接跳过onInterceptTouchEvent方法(既然子View都不要down事件了,更何况Move,Up事件),把intercepted 赋值为true
特殊情况:子View可以通过requestDisallowInterceptTouchEvent(boolean)方法来设置父ViewGroup无法拦截出了down以外的其它事件(设置父类标志位FLAG_DISALLOW_INTERCEPT实现),因为ViewGroup在Down事件来到时会对FLAG_DISALLOW_INTERCEPT进行重置,所以父类肯定能收到Down事件
事件分发部分(前提是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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
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;
}
}
遍历ViewGroup可以接受点击事件的子View(子View是否在播放动画和点击事件是否落在子View区域上),调用它们的dispatchTouchEvent,若有一个子View的dispatchTouchEvent返回true,则ViewGroup的mFirstTouchTarget 指向该View,跳出遍历,后面的点击事件都交给该子View处理
当事件传递给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)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
View的dispatchTouchEvent比较简单,首先会检查是否设置了OnTouchListener
- 有的话就去调用onTouch方法,如果onTouch返回true,则给result赋值true,导致它的onTouchEvent方法执行不了,所以onTouch方法优先级比onTouchEvent方法高,如果onTouch返回false,则继续执行onTouchEvent
- 没有的话就去执行onTouchEvent方法
下面是View的部分onTouchEvent方法:
......
if(((viewFlags &CLICKABLE)==CLICKABLE ||
(viewFlags &LONG_CLICKABLE)==LONG_CLICKABLE)||
(viewFlags &CONTEXT_CLICKABLE)==CONTEXT_CLICKABLE){
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) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
......
解析:主要有两点
- 最上面的判断语句中如果View的CLICKABLE或LONG_CLICKABLE中有一个为true的话,则onTouchEvent方法就会返回true,消费事件,注意所有view的LONG_CLICKABLE默认为false,CLICKABLE的话看具体的控件,button默认为true,textview默认为false
- 在UP事件中,会调用performClick(),如果view设置了OnClickListener,那么在performClick()里面会调用onClick方法
perfirnClick()方法如下:
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;
}
到此View的事件分发机制就分析结束了,在这里总结一下:
- 当点击事件发生传递到顶级View时(根ViewGroup),调用它的dispatchTouchEvent方法,然后在调用拦截方法询问是否拦截
- 拦截,调用自己onTouchEvent,后面的同一序列事件来到时,不在调用拦截方法,直接调用自己onTouchEvent方法
- 不拦截,遍历子View看谁需要处理事件,调用子View的dispatchTouchEvent方法(看2)
- 调用View的dispatchTouchEvent方法时
- 如果设置onTouch方法,则先调用onTouch,若返回true,事件消费,onTouchEvent无法被执行;若返回false,onTouchEvent执行
- 如果没有设置onTouch方法,则调用onTouchEvent,返回true则消费,后面其它同一序列事件都给这个View处理,若返回false则让父ViewGroup继续遍历其它子View
- 调用onTouchEvent方法时:
- 如果该View的CLICKABLE或LONG_CLICKABLE中有一个为true的话,则返回true消费事件
- 如果有设置OnClickListener的话,会在performClick()中调用该View的onClick()