Android事件分發機制,你瞭解過嗎?

ClericYi發表於2020-01-27

Android事件分發機制,你瞭解過嗎?

前言

CVTE一面答的很不好的題目之一,特別寫一篇部落格反思自己。我記得我當時答的糊里糊塗的,只說了連事件從什麼時候開始響應的也沒說,只說了從子控制元件開始接受到,如果不消費,或者沒有子控制元件能夠消費時,就向上傳遞,一直傳到根佈局後自動消費。

思維導圖

Android事件分發機制,你瞭解過嗎?

事件序列解析

所謂的安卓事件是什麼?具體來說的就是點選和滑動兩個操作;抽象著來說就是下面的表格。

MotionEvent/事件型別 具體操作
ACTION_DOWN 點下View
ACTION_UP 抬起View
ACTION_MOVE 滑動View
ACTION_CANCEL 非人為因素取消

事件序列一般組成:

點選的事件組成就是:Down --> Up

滑動的事件組成就是:Down --> Move --> Move .... --> Up

事件分發

  1. 使用到的函式
    • dispatchTouchEvent():用於事件分發
    • onTouchEvent():消費事件
    • onInterceptTouchEvent():判斷是否攔截事件,僅存在於ViewGroup
  2. 分發物件
    • Activity
    • ViewGroup
    • View

Activity的事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 從判斷語句中可以得出所有事件的起點就是Down
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 實現屏保功能
        onUserInteraction();
    }
    // 向上傳遞至ViewGroup,呼叫其dispatchTouchEvent
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
複製程式碼

文章中我已經新增了註釋內容,其中getWindow()獲得就是一個Window抽象類,根據其子類PhoneWindow我們可以很容易得知最後呼叫的其實就是ViewGroupdispatchTouchEvent()方法

/**
 * 實際上就是判斷事件是否是DOWN事件,event的座標是否在邊界內等
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}
複製程式碼

最後就是Activity中的onTouchEvent()方法了,這個模組乾的事情在註釋中也就很清晰明瞭了。

ViewGroup的事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
    ········
    // 初始化Down事件
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 丟棄之前手頭上乾的事情,重新開始響應Down事件
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    // 檢查是否需要攔截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 這個與運算是用於影響除Down以外的事件的
        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 {
        // 當前按壓的位置沒有控制元件,或者當前控制元件並不可被點選,直接被ViewGroup攔截
        intercepted = true;
    }
    ········
    /**
     *這個判斷裡面同樣的還是判斷響應的事件,然後就是通過一個for迴圈判斷位置來判斷當前的子控制元件是否在對應的位置內
     * 還有非常重要的一點就是這個迴圈的判斷還是倒敘的
     */
    if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
        ········
        if (newTouchTarget == null && childrenCount != 0) {
            ········
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                // 後面的篇幅主要用於判斷當前的控制元件的各種屬性是否是滿足需要的。比如說位置、是否可以點選、是否隱藏等一系列資訊
                ········ 
            }
   ········
}
複製程式碼
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;
}
複製程式碼

onInterceptTouchEvent(MotionEvent ev)函式可知,預設其實並不會去攔截。所以就一般情況而言,dispatchTouchEvent()方法是需要去迴圈遍歷子控制元件集合去尋找對應的控制元件的。

使用一個虛擬碼解釋以上的邏輯

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        // 自己攔截,自己消費
        onTouchEvent(ev);
    }else{
        // 不攔截,分發給子View進行消費
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
複製程式碼

View事件分發

    public boolean dispatchTouchEvent(MotionEvent event) {
        ·····
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        
        if (onFilterTouchEventForSecurity(event)) {
            ·····
            //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;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return result;
    }
複製程式碼

通過以往的實踐,我們知道只有通過設定了監聽器的View才能夠去監聽事件,那麼在dispatchTouchEvent()方法中也是一樣的,如果View並沒有被設定監聽器,變數result也不會被賦值成為true。

從程式碼中很容易看出onTouch()方法的優先順序大於onTouchEvent()方法。

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        ·····
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ·····
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ·····
                    }
                    ·····
                    mIgnoreNextUpEvent = false;
                    break;
                ·····
            }
            return true;
        }
        return false;
    }
複製程式碼

onTouchEvent()方法中其實具體幹了一件事情,那就是區別到底是長按事件還是點選事件。

那麼先行判斷的是長按事件還是點選事件呢?答案很明顯,在程式碼行中removeLongPressCallback();有一個這樣的函式,這就是去除長按事件回撥的函式,所以答案就是長按事件是第一個被判斷的事件,然後才是點選事件。

判斷這個方法的事件的方法就是通過做出Up動作時的時間和做出Down動作時的時間間隔。如果Down和Up兩個動作之間的時間間隔小於500ms,就是點選事件。

總結

Android事件分發機制.png

以上就是我的學習成果,如果有什麼我沒有思考到的地方或是文章記憶體在錯誤,歡迎與我分享。


相關文章推薦:

面試中的HashMap、ConcurrentHashMap和Hashtable,你知道多少?

還不會七大排序,是準備家裡蹲嗎!?

手撕ButterKnife

相關文章