Android事件分發機制——從基礎深入源碼解析

前言

前段時間找工作,看了好多關于事件分發機制的書,各路大牛從不同的角度進行了分析。本人受益匪淺,于是有了這篇吸取天地之精華的解析。

本文章會從什么是事件分發機制開始,一直深入到源碼分析
主要目的是讓自己理解更深入,也希望能讓讀者更容易讀懂而不覺干澀。

概念

本節都是基礎,我化身十萬個為什么提出以下幾個問題!如果讀者都明了那就直接跳向下一節!

  • 事件分發機制是什么?
    事件分發機制就是點擊事件的分發

  • 那么點擊事件又是什么?
    在手指接觸屏幕后產生的同一個事件序列都是點擊事件。

  • 點擊事件分為哪幾種類型?

    • 手指剛接觸屏幕
    • 手指在屏幕上滑動
    • 手指從屏幕上松開的一瞬間
  • 同一個事件序列是什么?
    是從手指接觸屏幕的一瞬間起,直到手指從屏幕上松開的一瞬間所產生的一切事件。

  • 點擊事件用代碼如何表示?
    在源碼中MotionEvent就是點擊事件,對點擊事件的分發就是對MotionEvent對象的分發傳遞過程

  • MotionEvent的點擊事件類型?

    • ACTION_DOWN:手指剛接觸屏幕
    • ACTION_MOVE:手指在屏幕上滑動
    • ACTION_UP:手指從屏幕上松開的一瞬間
  • 那這個MotionEvent到底是如何傳遞的?
    那就來看下一節!

事件分發機制

所謂事件分發機制,其實就是對MotionEvent(點擊事件)的分發過程。
當一個MotionEvent(點擊事件)產生之后,系統需要把它傳遞給一個具體的View,這個傳遞過程就是事件分發機制。

1. 我們來簡單描述一次點擊事件(不涉及方法調用,先有個大概的體系)

  • 用戶接觸屏幕產生MotionEvent(點擊事件)
  • MotionEvent(點擊事件)總是由Activity先接收
  • Activity接收后將MotionEvent(點擊事件)進行傳遞:Activity->Window->DecorView(DecorView是當前界面的底層容器,就是setContentView所設置View的父容器)
  • DecorView是一個ViewGroup,將MotionEvent(點擊事件)分發向各個子View

2. 三個方法
相信大家對點擊事件已經有所了解,那接下來我們介紹事件分發機制很重要的三個方法,點擊事件的分發機制都是根據這三個方法共同完成的:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。

  • dispatchTouchEvent()用來進行事件的分發,如果MotionEvent(點擊事件)能夠傳遞給該View,那么該方法一定會被調用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同決定。

    • 返回值為true,則表示該點擊事件被本身或者子View消耗。
    • 返回值為false,則表示該ViewGroup沒有子元素,或者子元素沒有消耗該事件。
  • onInterceptTouchEvent():在dispatchTouchEvent()中調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那么在同一個事件序列中不會再訪問該方法。

  • onTouchEvent():在dispatchTouchEvent()中調用,返回結果表示是否消耗當前事件,如果不消耗(返回false),則在同一個事件序列中View不會再次接收到事件。

3. 三個方法的關系
這么多概念,別頭疼!咱們用偽代碼看一下三個方法的關系!

public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean handled = false;
        if (onInterceptTouchEvent(ev)) {
            handled = onTouchEvent(ev);
        } else {
            handled = child.dispatchTouchEvent(ev)
        }
        return handled;
}  

這段偽代碼可以很好地理解事件的傳遞機制:
用戶點擊屏幕產生MotionEvent(點擊事件),View的dispatchTouchEvent()接收MotionEvent(點擊事件)后,先執行該View的onInterceptTouchEvent()判斷是否攔截該事件,若攔截執行該View的onTouchEvent()方法,若不攔截則調用子View的dispatchTouchEvent()。在事件傳遞的源碼中,使用的就是類似的邏輯。

4. 事件傳遞順序

  • 用戶點擊屏幕產生MotionEvent(點擊事件)
  • Activity接收MotionEvent(點擊事件)—>傳遞給Window—>傳遞給DecorView(ViewGroup)—>執行ViewGroup的dispatchTouchEvent()
  • ViewGroup接收到MotionEvent(點擊事件)之后,按照事件分發機制去分發事件。
  • 若當子View不消耗事件,onTouchEvent()返回false,那么這個事件會傳遞回其父View的onTouchEvent(),如若父View也不消耗,最后會傳遞回給Activity進行處理。

總的來說點擊事件的傳遞順序是由父到子,再由子到父的。

圖解事件傳遞機制

現在網上的大部分文章都是通過源碼和log講解事件的傳遞,對看文章的人來說體驗并沒有那么好,看的云里霧里摸不出個頭。在這獻上一本葵花寶典!看了這張圖媽媽再也不用擔心我的學習啦!

事件傳遞機制圖解

友情提示:

  1. 還是不理解的同學可以對照上一部分一起看效果更佳。
  2. 圖中View的onTouchEvent返回false,將事件傳遞給ViewGroup的過程,并不是直接傳遞。是上級ViewGroup的dispatchTouchEvent()方法接收到子View的onTouchEvent()返回的false,再將事件分發給自己(ViewGroup)的onTouchEvent。
  3. ViewGroup里面沒有復寫onTouchEvent,然而ViewGroup本身就是View,View中有onToucheEvent。

源碼解析

看了這么久咱們終于來看源碼啦!不多廢話!一庫!

右手左手來幾行源碼

1. Activity對點擊事件的分發
先來看Activity的dispatchTouEvent,所有點擊事件接收的源頭

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

這段代碼中我們著重看getWindow().superDispatchTouchEvent(ev),方法將點擊事件傳遞給了Window。返回值表示是否消耗掉了該點擊事件。如果所有的View都沒有消耗掉點擊事件,則Activity調用自己的onTouchEvent。

再來看Window的源碼:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

發現其實是一個接口,那實現方法在哪?不急,不難找,源碼的最上方注釋里寫道

The only existing implementation of this abstract class is android.view.PhoneWindow,

該接口的唯一實現方法是PhoneWindow,那咱們再去看PhoneWindow的源碼:

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

是不是很熟!其實他也把這個鍋直接甩給了DecorView ,之前介紹過,DecorView是當前界面的底層容器,就是setContentView所設置View的父容器。所以再來看DecorView:

public boolean superDispatchTouchEvent(MotionEvent event) {
     return super.dispatchTouchEvent(event);
}

碼個蛋!竟然又傳遞出去了,這次是調用了super,而DecorView是繼承自ViewGroup,所以調用了ViewGroup的dispatchTouchEvent!那這樣咱們就先來瞧一瞧ViewGroup里的源碼!

2.ViewGroup對事件的分發
先來看ViewGroup中的dispatchTouchEvent中的一小段

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

咱們從頭開始看,MotionEvent.ACTION_DOWN 這個之前介紹過,那mFirstTouchTarget 是什么?后面的代碼表示,當ViewGroup的點擊事件被子View消耗,那mFirstTouchTarget就會指向該子View。所以如果事件被子View消耗 或者 是ACTION_DOWN事件,那就訪問該ViewGroup的onInterceptTouchEvent,如果不那就全部被當前ViewGroup攔截。換句話說,如果View決定攔截事件,那么這一個事件序列都會由這個View來處理。

那么大家也注意到FLAG_DISALLOW_INTERCEPT這個標志位,看起來它可以影響ViewGroup是否攔截該事件。這個標志位是通過requestDisallowInterceptTouchEvent()方法來設置的,一般用于子View中。當標志位設置之后ViewGroup將無法攔截除了ACTION_DOWN以外的事件了。為啥說除了ACTION_DOWN以外呢?因為dispatchTouchEvent每次接收到ACTION_DOWN都會初始化狀態,代碼如下。

// 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();
}

綜上所述,requestDisallowInterceptTouchEvent()方法不能影響ACTION_DOWN事件
總結一點,onInterceptTouchEvent()方法不一定會每次都執行,如果想對每個事件都進行處理,那還是在dispatchTouchEvent()里面處理吧

咱們繼續往下走,當該ViewGroup不攔截點擊事件的時候,事件會傳遞給他的子View:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    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)) {
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                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);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

以上這段是ViewGroup進行事件分發的主要代碼,看起來比較簡單。當ViewGroup有子View的時候,進行子View的遍歷,其中有一個判斷條件:

canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null) 

判斷當前點擊事件是否在子View的坐標范圍內,且子View沒有在坐標系中移動(執行動畫),如果子View符合以上兩個情況那么就把點擊事件傳遞給他處理。往下走,會看到這么一個判斷條件:

dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

這個方法其實就是用來將事件分發給子View的,來看一下這個方法的其中一段源碼你就會清晰很多:

final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

如果子View為null那就交給該ViewGroup的dispatchTouchEvent(),反之就將點擊事件交給該子View(也有可能是ViewGroup)處理,一次分發就完成了。

再跳回到之前那段超長代碼,如果dispatchTransformedTouchEvent()返回true,表明點擊事件被子View消耗,執行addTouchTarget()方法給最開始的mFirstTouchTarget賦值

如果遍歷完了所有的子View,點擊事件都沒有被消耗掉,可能有兩種情況:一、ViewGroup下面沒有子View。二、子View沒有消耗點擊事件。這兩種情況下,ViewGroup會自己處理點擊事件。當子View不消耗點擊事件,那點擊事件將交由給他的父View去處理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

代碼里面child參數賦值為null,當child為null時,訪問當前ViewGroup的super.dispatchTouchEvent(event),因為ViewGroup是繼承自View,所以其實訪問的就是View的dispatchTouchEvent()方法。

3.View對事件的分發
再來看看View的dispatchTouchEvent()方法的其中一段代碼,注意知識其中一段,篇幅不能太長,想要全部查看一定打開Studio看看源碼!

public boolean dispatchTouchEvent(MotionEvent event) {
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        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;
        }
    }
    return result;
}

View的dispatchTouchEvent()就比較簡單了,onFilterTouchEventForSecurity(event)是用來判斷點擊事件來到時,窗口有沒有被遮擋住,如果被遮擋住則直接返回false,不消耗事件。
反之,接收到事件后看到一個類ListenerInfo,那這是個啥?看源碼啊!

static class ListenerInfo {
    public OnClickListener mOnClickListener;

    protected OnLongClickListener mOnLongClickListener;

    private OnKeyListener mOnKeyListener;

    private OnTouchListener mOnTouchListener;
    
    ......
}

看完源碼發現它是一個View的靜態內部類,定義了一系列的Listener。
繼續看View的dispatchTouchEvent()的源碼發現,View會先判斷自己是否有設置OnTouchListener,如果所設置的OnTouchListener得onTouch返回true,則直接消耗點擊事件,不再執行onTouchEvent()方法。

得出一個結論,OnTouchListener的優先級高于onTouchEvent()。這樣做的好處是方便在外部處理事件。

如果沒有設置OnTouchListener那就會執行到View的onTouchEvent(),繼續看下onTouchEvent()的源碼,咱們一段一段來,有點長:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

當我們把View設置為不可用狀態,View依然會消耗點擊事件,只是看起來不可用。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

之后如果View設置有代理,那么就會直接執行代理的onTouchEvent()。下面再來看一下點擊事件的主要代碼:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    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 (prepressed) {
                    setPressed(true, x, y);
               }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    removeLongPressCallback();
                    if (!focusTaken) {
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            ......
            break;

        case MotionEvent.ACTION_CANCEL:
            ......
            break;

        case MotionEvent.ACTION_MOVE:
            ......
            break;
    }

    return true;
}

當View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE有其中一個為true那么View就會消耗掉這個事件。并且在ACTION_UP的時候會執行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);
    return result;
}

如果View設置了OnClickListener,performClick()這個方法就會去執行這個監聽事件。
再來一個結論,OnTouchListener的優先級高于OnClickListener,OnClickListener是在ACTION_UP的時候執行的。

看到這里事件傳遞機制的源碼分析終于結束了!!!

結論

  • 事件分發機制就是點擊事件的分發,在手指接觸屏幕后產生的同一個事件序列都是點擊事件。
  • 點擊事件的傳遞順序是由父到子,再由子到父的。
  • 正常情況下事件只能被一個View攔截。
  • 如果View決定攔截事件,那么這一個事件序列都會由這個View來處理。
  • 當子View不消耗點擊事件,那點擊事件將交由給他的父View去處理,如果所有的View都沒有消耗掉點擊事件,則Activity調用自己的onTouchEvent。
  • onInterceptTouchEvent()方法不一定會每次都執行,如果想對每個事件都進行處理,那還是在dispatchTouchEvent()里面處理吧。
  • OnTouchListener的優先級高于onTouchEvent()。這樣做的好處是方便在外部處理事件。
  • 當我們把View設置為不可用狀態,View依然會消耗點擊事件,只是看起來不可用。

最后給大家推薦一篇View源碼分析的文章,里面有Log日志分析。大家可以看一看增深理解。《Android View 事件分發機制源碼詳解(View篇)》

最最后再給大家推薦一本書《Android開發藝術探索》,各大網站都有賣,對于突破瓶頸有很大的意義。

結后談

博主花了一段時間終于理順完了這篇文章,當然由于博主的技術原因,文章并不是十全十美的,只希望給還處在迷茫期的朋友們指引一條方向。
希望我的文章能給大家帶來一點點的福利,那在下就足夠開心了。
下次再見!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容