Android的MotionEvent事件分發機制

長孫雨聰七星上將發表於2019-03-04

個人部落格:http://zhangsunyucong.top

android事件的源頭在哪裡?

當使用者觸控螢幕或者按鍵等時,形成事件,事件經過linux底層Event節點捕獲之後,一直傳到android應用層。中間傳遞的過程不是本文的重點,我也不是很清楚(哈哈哈)。本文的重點是事件在應用層的分發機制。

事件在View樹中的分發過程

View樹:

圖片

在Android中,事件的分發過程就是MotionEvent在view樹分發的過程。預設是中從上而下,然後從下而上的傳遞的,直到有view、viewgroup或者Activity處理事件為止。

為什麼要先從上而下?是為了在預設情況下,螢幕上層疊的所有控制元件都有機會處理事件。這個階段我們稱為事件下發階段。

為什麼要從下而上?是為了在從上而下分發時,事件沒有控制元件處理時,再從下而上冒泡事件,是否有控制元件願意處理事件。如果中間沒有控制元件處理,事件就只能由Acitivity處理了。這個階段我們稱為事件的冒泡階段。

準備

事件序列:從使用者手指觸控螢幕開始,經過滑動到手指離開螢幕。這個操作產生了一個dowm事件,一系列move事件,最後一個up事件結束。我們把這一個操作產生的事件稱為一個事件序列。

Acitivity中和事件傳遞有關的函式
事件分發:dispatchTouchEvent
事件處理:onTouchEvent

ViewGrop中和事件傳遞有關的函式
事件分發:dispatchTouchEvent
事件攔截:onInterceptTouchEvent
事件處理:onTouchEvent

View中和事件傳遞有關的函式
事件分發:dispatchTouchEvent
事件處理:onTouchEvent

從上面可以看出,ViewGrop中多了事件攔截onInterceptTouchEvent函式,是為了詢問自己是否攔截事件(在事件分發中詢問),如果沒有攔截就傳遞事件給直接子view,如果攔截就將事件交給自己的事件處理函式處理。View中沒有事件攔截函式,因為view是在view樹中的葉節點,已經沒有子view。

下面是先進行原始碼分析,然後再驗證得出一些結論。程式碼遲點上傳github。
用圖表示佈局的層級關係:

圖片

這裡分析事件的分發過程,是從down事件的分發開始,以及分析它在兩個階段的傳遞過程:下發階段和冒泡階段。

事件下發階段

(1)在Acitvity中的原始碼分析:

Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}
複製程式碼

在第4行,Acivity將事件傳遞給了Window,Window是一個抽象類。在手機系統中它的實現是PhoneWindow.下面進入PhoneWindow中。

PhoneWindow#superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製程式碼

從上面可以看出,事件已經從Acitivity到PhoneWindow,再傳到了DecorView。DecorView是一個繼承FrameLayout的ViewGroup,從而事件進入了View樹的傳遞。

重寫在Acitvity中的事件傳遞方法

重寫Activity#dispatchTouchEvent:
1、返回false,事件不分發,所有事件在Acitivity的分發函式中就中斷(真的不見了),連Acitivity的事件處理函式都到達不了。
2、返回true,所有事件在Acitivity的分發函式中就中斷,和false一樣
3、返回父函式方法,事件就傳給直接子view分發

(2)在ViewGruop中的原始碼分析:
ViewGruop#dispatchTouchEvent

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

// Check for interception.
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;
}
複製程式碼

在5-11行,是每個新的事件系列開始前,會重置事件相關的狀態。這裡我們關注兩個地方。第一個是第17行的disallowIntercept標誌,第二個是第19行呼叫了事件攔截函式,詢問是否攔截事件。

ViewGruop#onInterceptTouchEvent

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的程式碼很簡單。

重寫在ViewGroup中的事件傳遞方法
重寫ViewGroup#dispatchTouchEvent:
1、返回false,不分發,down事件給父ViewGroup處理,以後的事件全部直接通過父ViewGroup分發函式給父ViewGroup的事件處理函式處理。
2、返回true,則所有的事件都從頭來到這裡就中斷,不見了。
3、返回父函式方法,看下面攔截函式

重寫ViewGroup#onInterceptTouchEvent(詢問是否攔截):
1、返回true,就呼叫處理函式,在處理函式中是否消耗down事件
2、返回false,是否是最後一個view?否,down事件就分發給子View;是,就呼叫一次它的處理函式,進入冒泡階段(就是一寸事件處理函式呼叫)
3、返回父函式的方法,和返回false一樣

重寫ViewGroup的onTouchEvent,當down事件來到中onTouchEvent時,
1、返回true,就消耗down事件,後面全部事件從頭分發到處理函式(不用再詢問是否攔截)。後面的事件根據是否消耗而是否消失(不消耗就消失),消失的所有事件由Acitivity處理(注意消失的事件也是從頭傳遞到這裡再傳給Acitivity的)。
2、返回false,將down事件冒泡回去,看誰會處理。
3、返回父函式方法,是預設不消耗。

(3)在View中的原始碼分析:
View#dispatchTouchEvent

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)) {
                result = true;
    }

    if (!result && onTouchEvent(event)) {
        result = true;
    }
}
複製程式碼

這裡關注的地方是,第9行和第13行。第9行是當前view如果設定了onTouch事件,並且它返回了true,那它就直接將result設定為true,事件就消耗了,不會再繼續傳遞下去,只到達onTouch。第13行,是事件處理函式。可以看出onTouch是優先於onTouchEvent的。

View#onTouchEvent

....
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:
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        if ((viewFlags & TOOLTIP) == TOOLTIP) {
            handleTooltipUp();
        }
        if (!clickable) {
            removeTapCallback();
            removeLongPressCallback();
            mInContextButtonPress = false;
            mHasPerformedLongPress = false;
            mIgnoreNextUpEvent = false;
            break;
        }
        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
            // take focus if we don`t have it already and we should in
            // touch mode.
            boolean focusTaken = false;
            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                focusTaken = requestFocus();
            }

            if (prepressed) {
                // The button is being released before we actually
                // showed it as pressed.  Make it show the pressed
                // state now (before scheduling the click) to ensure
                // the user sees it.
                setPressed(true, x, y);
            }

            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) {
                    // Use a Runnable and post this rather than calling
                    // performClick directly. This lets other visual state
                    // of the view update before click actions start.
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }
                    if (!post(mPerformClick)) {
                        performClick();
                    }
                }
            }
            ...
        }
        ...
    }
    return true;
}
    
複製程式碼

view根據是否可以點選等等一系列判斷什麼的。這裡關注up事件中的第42-53行,有performClick。

View#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;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}
複製程式碼

如果view設定了mOnClickListener,即點選事件,會呼叫view的點選事件。如果在父view中攔截了up事件,使up事件到達不了這裡,會使view的點選事件失效。

可以知道,onTouch是優先於onTouchEvent,onTouchEvent優先於onclick。

事件冒泡階段

當down事件到達了最後一個子view,如果仍然沒有view願意處理它,就呼叫一次最後一個子view的事件處理函式,是否處理dowm事件,如果不處理,就一直冒泡回去,直到有view的onTouchEvent處理為止。如果都不處理,就只有Acitivity自己處理了。整個事件冒泡階段就是一串onTouchEvent的回溯過程,自下而上。

相關文章