Android的MotionEvent事件分發機制

我的博客:http://zhangsunyucong.toplinux

android事件的源頭在哪裏?

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

事件在View樹中的分發過程

View樹: git

圖片

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

爲何要先從上而下?是爲了在默認狀況下,屏幕上層疊的全部控件都有機會處理事件。這個階段咱們稱爲事件下發階段。bash

爲何要從下而上?是爲了在從上而下分發時,事件沒有控件處理時,再從下而上冒泡事件,是否有控件願意處理事件。若是中間沒有控件處理,事件就只能由Acitivity處理了。這個階段咱們稱爲事件的冒泡階段。app

準備

事件序列:從用戶手指觸摸屏幕開始,通過滑動到手指離開屏幕。這個操做產生了一個dowm事件,一系列move事件,最後一個up事件結束。咱們把這一個操做產生的事件稱爲一個事件序列。ide

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: 一、返回false,事件不分發,全部事件在Acitivity的分發函數中就中斷(真的不見了),連Acitivity的事件處理函數都到達不了。 二、返回true,全部事件在Acitivity的分發函數中就中斷,和false同樣 三、返回父函數方法,事件就傳給直接子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: 一、返回false,不分發,down事件給父ViewGroup處理,之後的事件所有直接經過父ViewGroup分發函數給父ViewGroup的事件處理函數處理。 二、返回true,則全部的事件都從頭來到這裏就中斷,不見了。 三、返回父函數方法,看下面攔截函數

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

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

(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的回溯過程,自下而上。

相關文章
相關標籤/搜索