事件分發機制學習筆記

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了,之後的一系列事件都會交給該View處理
值得注意的是:如果一個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;
            }
發佈了32 篇原創文章 · 獲贊 7 · 訪問量 4920
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章