Android菜鳥一枚,做項目的時候經常碰到滑動衝突,於是痛下狠心學了一下事件分發機制,並且通過翻看源碼,稍有心得。
事件分發的基礎
說到事件的分發機制,不得不提到三個方法:
dispatchTouchEvent(MotionEvent ev);
onInterceptTouchEvent(MotionEvent ev);
onTouchEvent(MotionEvent event);
在ViewGroup中三個方法全部存在,而在View中,不存在onInterceptTouchEvent(MotionEvent ev);
當點擊事件發生的時候,事件首先會傳給activity,此時activity的dispatchTouchEvent(MotionEvent ev)方法執行。然後事件傳給window,通過window將事件傳給頂級view。接下來就是我們的重點了。
我們都知道view是放在ViewGroup中的,而子ViewGroup又是放在父ViewGroup中,這樣一層一層就形成了View樹,當一個事件發生的時候,應該交給誰來處理呢?
有必要了解一下上面三個方法了:
《Android開發藝術探索》上有一段僞代碼把這三個方法的關係表現的淋漓盡致。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
dispatchTouchEvent(MotionEvent ev): 如果事件能傳到這裏的話,該方法一定會被調用,用於對事件進行分發。
onInterceptTouchEvent(MotionEvent ev): 如果事件能傳到該view,當action爲ACTION_DOWN的時候,該方法一定執行,用來詢問要不要攔截,如果確定攔截的話,事件傳到該view後便不會再往下傳,並且之後的一系列事件便不會走該方法;如果該View處理事件的話(ouTouchEvent返回值爲true),那麼之後的一系列事件都會交給該view處理;
onTouchEvent(MotionEvent event):該方法是大家在自定義控件中最經常重寫的方法,用於對事件進行處理。如果返回true的話,那麼down事件及其之後的一系列事件都交給所在的view執行;如果該view不處理的話,事件再上傳給父View的onTouchEvent方法,而且我測試後發現這種情況下,之後的一系列方法竟然不會再傳到該View了,而是傳給那個處理事件的View。
假設:ViewGroupFather—>ViewGroupChild—>View
那麼首先毫無疑問,事件會傳給ViewGroupFather,dispatchTouchEvent(MotionEvent ev)首先執行。事件傳遞的流程如下所示:
值得注意的是:如果一個View設置了OnTouchListener,OnTouchListener.onTouch()將會執行。僞代碼如下:
if (mOnTouchListener!=null){
if (!mOnTouchListener.onTouch(view, event)){
onTouchEvent(event);
}
}else {
onTouchEvent(event);
}
如果還設置有點擊監聽OnClickListener,將在onTouchEvent(event)的case ACTION_UP:case MotionEvent.ACTION_UP:{}中執行performClick()方法。從下面的代碼中,我們可以看到點擊事件的執行了。
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;
}
...
return result;
}
重要的都在dispatchTouchEvent(MotionEvent ev)中,應該好好看看該方法的代碼。
VewGroup的dispatchTouchEvent方法
從該方法的開始看,有以下代碼
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
上面代碼中我們來看resetTouchState()方法,該方法中調用了clearTouchTargets(),而在這個方法中有這麼一句代碼mFirstTouchTarget = null,對mFirstTouchTarget 清空。
接下來繼續看dispatchTouchEvent中的代碼:
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;
}
在上面代碼中,如果進入if語句塊,需要滿足兩個條件actionMasked == MotionEvent.ACTION_DOWN和mFirstTouchTarget != null,那麼當事件爲down時候,肯定會走這個語句塊。當事件爲move和up的時候就不好說了,這個時候需要看mFirstTouchTarget != null成不成立,上文知道當action爲down的時候爲null,那麼它什麼時候賦值呢?由下文可以知道,當ViewGroup的子元素處理事件的時候,mFirstTouchTarget會被賦值。也就是說如果當前ViewGroup攔截該事件,那麼onInterceptTouchEvent(ev)不再被調用,之後的一系列事件都會交給該ViewGroup處理。
但是,從代碼中我們可以看出來,當前ViewGroup如果想要走onInterceptTouchEvent(ev),還需要disallowIntercept爲false,也就是FLAG_DISALLOW_INTERCEPT爲false。這個標記位平常很少用到,但是requestDisallowInterceptTouchEvent()方法我們經常在ViewGroup中用到,這個方法便是設置mFirstTouchTarget的。
如果當前ViewGroup不攔截事件,將會走dispatchTouchEvent的以下代碼:
for (int i = childrenCount - 1; i >= 0; i--) {
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
上面代碼,我只是挑出的一些重點,並不是完整的代碼。
首先對ViewGroup中的子view進行遍歷,如果不符合條件的就直接continue了,不再往下走了。怎麼纔是符合條件呢?canViewReceivePointerEvents是判斷子元素是否在播放動畫,isTransformedTouchPointInView是判斷點擊的事件座標是否落在了子元素的區域內。
我們點進去看一下dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
當然此時child不爲空,將會走孩子的dispatchTouchEvent方法,並且在addTouchTarget(child, idBitsToAssign)中對mFirstTouchTarget進行賦值,跳出循環。
如果所有子元素都沒有處理,那麼接着走dispatchTouchEvent以下代碼:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
因爲子元素沒有處理,所以mFirstTouchTarget == null,進入dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS)。這個方法的代碼已經貼在上面,此時的child爲null。會走super.dispatchTouchEvent(event)。這裏就轉到了View的dispatchTouchEvent,而在View的dispatchTouchEvent中,不再分發事件,也沒有調用onInterceptTouchEvent(MotionEvent ev)方法的情況,而是走的OnTouchEvent。該部分在上文也已經提到,看看代碼就明白了:
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;
}