1. 程式人生 > >Android學習筆記之View的事件分發機制

Android學習筆記之View的事件分發機制

一、點選事件的傳遞規則

所謂的點選事件分發過程,其實就是當我們點選螢幕,產生了一個MotionEvent之後,系統將這個事件傳遞給一個具體View的過程。總的來說,事件總是先傳遞給Activity,然後傳遞給Window,再傳遞給頂級View(Activity→Window→DecorView),最後再按照事件分發機制一層一層向下去分發事件。而這個分發過程由三個很重要的方法來共同完成:

  1. dispatchTouchEvent(MotionEvent ev):用來進行事件的分發。如果事件能夠傳遞給當前View,那麼該方法一定會被呼叫,返回結果受當前View的onTouchEvent方法和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
  2. onInterceptTouchEvent(MotionEvent ev):在上述方法的內部呼叫,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會再次呼叫,返回結果表示是否攔截當前事件。
  3. onTouchEvent(MotionEvent ev):在 dispatchTouchEvent 方法中呼叫,用來處理點選事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

上述三個方法之間的關係可以通過如下偽程式碼表示:

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. 所謂的一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以 down 事件開始,以 up 事件結束,中間含有多個 move 事件。
  2. 某個 View 一旦決定攔截,那麼這一個事件序列都只能由它來處理,並且它的 onInterceptTouchEvent 不會再被呼叫。
  3. 參考第2條,通常而言,一個事件序列只能被一個 View 攔截消耗。但是通過特殊手段,可能出現多個 View 處理的情況,比如一個 View 將本該自己處理的事件通過 onTouchEvent 強行傳遞給其他View處理。
  4. 某個View一旦開始處理事件(onTouchEvent 開始執行),如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回 false),那麼同一事件序列中的其他事件都不會再交給它處理,並且事件將重新交由它的父元素去處理(即父元素的 onTouchEvent 會被呼叫)。在這裡,如果所有 View 的 onTouchEvent 返回 false,那麼最終會傳遞給 Activity 處理(即Activity 的 onTouchEvent 會被呼叫)。
  5. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點選事件會消失,此時父元素的 onTouchEvent 並不會被呼叫,並且當前View可以持續受到後續的事件,最終這些消失的點選事件會傳遞給 Activity 處理。
  6. ViewGroup 預設不攔截任何事件。
  7. View 沒有 onInterceptTouchEvent 方法,一旦有點選事件傳遞給它,那麼它的 onTouchEvent 方法就會被呼叫。
  8. View 的 onTouchEvent 預設都會消耗事件(返回 true),除非它是不可點選的(clickable、longClickable、contextClickable都為 false)非提示框控制元件。View 的 longClickable、contextClickable屬性預設都為false,但 clickable 屬性要視情況:比如 Button 為 true,TextView 為 false。
  9. View 的 enable 屬性不影響 onTouchEvent 的預設返回值。哪怕一個View是disable狀態的,只要它的 clickable、longClickable、contextClickable不同時為 false,其 onTouchEvent 就預設返回 true。
  10. onClick 會發生的前提是當前 View 是可點選的,並且它收到了 down 和 up 的事件。
  11. 事件的傳遞過程是由外向內的。通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,但是 ACTION_DOWN 事件除外。

二、事件分發的原始碼分析 

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 方法。