Android源碼系列六:事件分發機制

基礎

在分析事件分發之前,我們先來了解三個相關的重要知識點:

  1. 事件分發的對象是什麼?
  2. 誰在分發?
  3. 依賴於什麼分發?

我們先把這三個問題弄清楚了再來看具體的原理:

  1. 事件分發的對象就是我們的觸摸屏幕產生的 Touch 事件。最常見的事件類型爲:down、up、move和cancel。

    • MotionEvent.ACTION_DOWN:觸摸到屏幕所立即產生的事件類型
    • MotionEvent.ACTION_UP:離開屏幕所產生的事件類型
    • MotionEvent.ACTION_MOVE:在屏幕上移動所產生的事件類型
    • MotionEvent.ACTION_CANCEL:事件被取消,非人爲的取消,比如:關機、鎖屏
  2. 事件分發是在 ActivityViewGroupView 三者之間傳遞的過程,事件先傳遞到 Activity,接着傳遞到 ViewGroup,最後傳給了 View。如下圖 ① -> ② -> ③:

  3. 事件分發主要就是通過三個方法在上述三者之間傳遞:

    • dispatchTouchEvent(ev: MotionEvent?): Boolean:分發、派發事件的方法
    • onInterceptTouchEvent(ev: MotionEvent?): Boolean:攔截事件的方法
    • onTouchEvent(event: MotionEvent?): Boolean:消費事件的方法

    通過下方的表格來認識一下三個方法和三者之間的擁有關係:

    dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()
    Activity 可以分發 無攔截方法 可消費
    ViewGroup 可以分發 有攔截方法 可消費
    View 可以分發 無攔截方法 可消費

    理解了上面三個問題之後,我們就可以放心大膽的去通過源碼去了解事件分發的原理了。

入口 Activity

首先看看 ActivitydispatchTouchEvent() 方法:

/**
 * 事件分發的入口,可以override
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 如果事件類型爲down,那麼先執行一遍onUserInteraction()
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // getWindow()是獲取Window對象,但是Window是抽象類,唯一實現類爲PhoneWindow
    // 可直接查看PhoneWindow.superDispatchTouchEvent(ev)方法
    // 順藤摸瓜下去就可以看到實際上調用的是ViewGroup.dispatchTouchEvent(ev)方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

/**
 * 用於和用戶交互
 * 此方法爲空方法,可override
 */
public void onUserInteraction() {
}

/**
 * 當前Activity下的任何一個View都沒有處理點擊事件,就會執行此方法
 * 此方法用於處理Window邊界外的點擊事件
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

Activity 的事件分發代碼還是比較簡潔的,我們大致理一下流程:

  • 接收到 down 事件,執行 onUserInteraction()
  • 如果 ViewGroup.dispatchTouchEvent(ev) 返回爲 true,那麼就直接返回 true,結束;
  • 如果 ViewGroup.dispatchTouchEvent(ev) 返回爲 false ,調用自身的 onTouchEvent(ev) 方法,結束。

結合下方的流程圖更容易理解:


中間人ViewGroup

理解了 Activity 對事件的處理之後,我們趁熱打鐵來分析一下 ViewGroup 對事件的分發、攔截和消費,因爲它涉及到了三個方法,可想而知難度會提升一點。

來看看 ViewGroup 是如何分發、攔截和消費事件的:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 標誌是否分發了此點擊事件
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        // 判斷是否攔截此點擊事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // disallowIntercept = 是否禁用事件攔截的功能(默認是false),可通過調用requestDisallowInterceptTouchEvent()修改
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // 可以攔截
            if (!disallowIntercept) {
                // 調用攔截事件方法,默認返回false
                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;
        }
        // 如果沒有被取消而且沒有被自身攔截,那就向下(子View)分發
        if (!cancel && !intercepted) {
            // 循環每個子View,調用子View的dispatchTouchEvent
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);
                // 實際就是調用子View的dispatchTouchEvent(ev)
                if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
                    // 如果子View分發成功,那麼將child賦值到mFirstTouchTarget對象的next指針中
                    // mFirstTouchTarget是TouchTarget對象的引用,TouchTarget類似鏈表結構
                    addTouchTarget(child,idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                }
            }
        }
        // 如果mFirstTouchTarget爲空,說明沒有執行上面的addTouchTarget(child,idBitsToAssign)方法
        // 那就代表事件要麼被取消了,要麼被攔截了,這時候dispatchTransformedTouchEvent()第三個參數傳的是null,會在下面介紹
        if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        }else{
            // alreadyDispatchedToNewTouchTarget在上面循環的時候,如果分發成功,它就爲true,handled就爲true
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    }
        }
        return handled;
    }
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            // 如果子View爲空,那麼就調用super.dispatchTouchEvent(ev) == View.dispatchTouchEvent(ev)
            // ViewGroup是View的子類,所以在super中的事件將被ViewGroup代替
            handled = super.dispatchTouchEvent(event);
        } else {
            // 子View不爲空,調用View.dispatchTouchEvent(ev)
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
}

/**
 * 可覆寫,返回true表示攔截,false表示不攔截
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

上面的註釋一定要仔細看,看完如果還有點不理解,沒關係,我們接着理一下 ViewGroup 的整體思路:

  • dispatchTouchEvent() 方法中,首先執行 onInterceptTouchEvent(ev) 方法,看是否被攔截;
  • 如果被攔截,那麼就不分發事件到子 ViewdispatchTouchEvent(ev),而是分發到 super.dispatchTouchEvent() ,由於 ViewGroup 的父類就是 View,所以還是會執行 View.dispatchTouchEvent(ev) 方法,這裏先不急着弄懂這步,只要知道流程就行,將會在下節介紹;
  • 如果沒有被攔截,直接循環子 View ,調用子 ViewdispathcTouchEvent(ev) 方法。

到這裏大致的流程就清晰了,再結合下方的流程圖加深下理解:


最後接收人View

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        ListenerInfo li = mListenerInfo;
        // 監聽信息不爲空
        // OnTouchListener不爲空,也就是setOnTouchListener()
        // 當前view的enable爲true
        // OnTouchListener接口中的boolean onTouch(View v, MotionEvent event)方法需要覆寫
        // 缺一不可
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 如果上面不滿足,那麼看onTouchEvent方法是否返回true,如果返回true,那麼result也爲true,否則爲false
        // 其實這個if()也就等於:return onTouchEvent(event),仔細體會下
        if (!result && onTouchEvent(event)) {
            result = true;
        }
        return result;
    }

    public boolean onTouchEvent(MotionEvent event) {
        // 只要設置了單擊和長按事件,clickable就爲true
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            // 調用點擊事件
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {
                performClickInternal();
            }
            return true;
        }
        return false;
    }

View 的事件分發相當於 ViewGroup 就簡單多了,流程在源碼中也體現的很清楚,我們來理一下整個流程:

  • 執行到 dispatchTouchEvent(ev) 的時候,先去調用 onTouch(view,ev) 方法;
  • 如果 onTouch() 返回 false,纔去調用下面的 onTouchEvent(ev) 方法;
  • onTouchEvent(ev) 方法內部,可以看見執行了 performClickInternal() 方法,這個方法就是我們常見的 onClick()

以上三點都是需要滿足種種條件纔可執行的,條件都在源碼註釋中詳細解釋了,大家一定要把這些條件看清楚了。

到這裏我們需要解決在 ViewGroup 中留下的一個問題,那麼就是當 ViewGroup 沒有子 View 的時候,調用了一個 super.dispatchTouchEvent(ev) 方法,其實就是把 ViewGrouponTouch()onTouchEvent()onClick() 三個方法按照條件來執行,千萬不要以爲調用父類的方法還是執行子 View 的事件分發!

View 的事件分發主要流程參考下方的流程圖:


事件分發的流程說難吧其實理清了也不難,說不難吧還是挺繞的,最後總結下幾點:

  • 事件分發的順序是:Activity -> ViewGroup -> View

  • ActivityView 都沒有攔截事件,Activity 如果存在攔截事件,那麼整個頁面都響應不了點擊事件了,View 因爲是最後一層,攔不攔截都沒必要了;

  • ViewGroup 在無子 View 接收分發事件或子 View 分發事件返回 false 的時候,會調用自身的 onTouchonTouchEvent()onClick() 方法。

大家一定要好好體會Android事件分發機制,無論是在日常開發中還是面試中,都是必不可缺的知識點!源碼分析的文章還在不斷的更新,如果本文章你發現有不正確或者不足之處,歡迎你在下方留言或者掃描下方的二維碼留言也可!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章