Android 事件分發(fā)機(jī)制源碼和實(shí)例解析

  1. 事件分發(fā)過程的理解
    1.1. 概述
    1.2. 主要方法
    1.3. 核心行為
    1.4. 特殊情況

  2. 案例分析
    2.1. 案例1:均不消費(fèi) down 事件
    2.2. 案例2:View0 消費(fèi) down 事件
    2.3. 案例3:ViewGroup2nd 消費(fèi) down 事件

  3. down 事件分發(fā)圖

1. 事件分發(fā)過程的理解

1.1. 概述

事件主要有 down (MotionEvent.ACTION_DOWN),moveMotionEvent.ACTION_MOVE),upMotionEvent.ACTION_UP)。
基本上的手勢(shì)均由 down 事件為起點(diǎn),up 事件為終點(diǎn),中間可能會(huì)有一定數(shù)量的 move 事件。這三種事件是大部分手勢(shì)動(dòng)作的基礎(chǔ)。

事件和相關(guān)信息(比如坐標(biāo))封裝成 MotionEvent。

大體的分發(fā)過程為:首先傳遞到 Activity,然后傳給了 Activity 依附的 Window,接著由 Window 傳給視圖的頂層 View 也就是 DecorView,最后由 DecorView 向整個(gè) ViewTree 分發(fā)。分發(fā)還會(huì)有回溯的過程。最后還會(huì)回到 Activity 的調(diào)用中。

Activity 的分發(fā)事件源碼

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

getWindow().superDispathTouchEvent 就是用來分發(fā)事件到 DecorView 中。如果整個(gè) ViewTree 沒有消費(fèi)事件,會(huì)調(diào)用 Activity 的 onTouchEvent。

1.2. 主要方法

1.2.1. 概覽

主要涉及到的 View 或 ViewGroup 的方法有:

dispatchTouchEvent該方法封裝了事件分發(fā)的整個(gè)過程。是事件分發(fā)的 調(diào)度者指揮官 。的核心過程均在該方法中。下面的 onInterceptTouchEventonTouchEvent 的回調(diào)的調(diào)用就在該方法體中。是否傳遞事件到
onInterceptTouchEventonTouchEventdispatchTouchEvent 決定。

onInterceptTouchEvent,該方法決定了是否攔截事件。只有 ViewGroup 有該回調(diào)。返回 true 表示攔截,返回 false 表示不攔截。自定義 View 的時(shí)候,可以重載該方法,通過一些特定的邏輯來決定是否攔截事件。如果攔截,接下來會(huì)調(diào)用該 ViewGroup 的 onTouchEvent 來處理事件。

onTouchEvent,該方法處理了事件,并決定是否繼續(xù)消費(fèi)后續(xù)事件。該方法調(diào)用的前置條件:

  • 該 View 攔截了事件
  • 子 View 都不消費(fèi)事件
  • 沒有子 View

該方法正式處理 MotionEvent。返回 true 表示消費(fèi),返回 false 不消費(fèi)。如果消費(fèi),接下來的事件還會(huì)傳遞到該 View 的 dispatchTouchEvent 中;如果不消費(fèi),后面的事件不會(huì)再傳過來。

onTouchListeneronTouch 回調(diào),和 onTouchEvent 一樣,優(yōu)先級(jí)比 onTouchEvent 高,如果有設(shè)置該監(jiān)聽,并且 onTouch 返回 true,就不會(huì)再調(diào)用 onTouchEvent 了。如果返回 false,事件還是會(huì)傳遞到 onTouchEvent 中。

<h4 id="1.2.2"> 1.2.2. dispatchTouchEvent 方法中的一些細(xì)節(jié)處理:</h4>

大部分手勢(shì)的起點(diǎn)為 down 事件,dispatchTouchEvent 如果收到 down 事件,會(huì)重新設(shè)置一些變量和標(biāo)記

重置變量和標(biāo)記的源碼

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

實(shí)際的源碼中,ViewGroup 繼承于 View。 當(dāng)子 View 不消費(fèi)事件或者 ViewGroup 攔截了事件會(huì)傳空值到 dispatchTransformedTouchEvent 中,內(nèi)部會(huì)調(diào)用 super.dispatchTouchEvent,最終把事件傳給 onTouchEvent 進(jìn)行處理。

dispatchTransformedTouchEvent 關(guān)鍵部分

// Perform any necessary transformations and dispatch.
if (child == null) {
    handled = super.dispatchTouchEvent(transformedEvent);
} else {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    transformedEvent.offsetLocation(offsetX, offsetY);
    if (! child.hasIdentityMatrix()) {
        transformedEvent.transform(child.getInverseMatrix());
    }
    
    handled = child.dispatchTouchEvent(transformedEvent);
}

也就是 dispatchTransformTouchEvent 完成了分發(fā)的最后過程:
a. 傳入的 child 不為空,轉(zhuǎn)化坐標(biāo)為 child 的坐標(biāo)系,調(diào)用 child.dispatchTouchEvent 向 child 分發(fā)事件
b. 傳入的 child 為空,調(diào)用 super.dispatchTouchEvent 分發(fā)事件到 onTouchEvent

<h4 id="1.2.3"> 1.2.3 方法的主要關(guān)系 </h4>

對(duì)于一個(gè) ViewGroup 來說,幾個(gè)重要方法的關(guān)系如下

幾個(gè)重要方法關(guān)系偽代碼

public boolean dispatchTouchEvent(MotionEvent e) {
    boolean consumed = false;
    if (onInterceptTouchEvent(e)) {
        consumed = onTouchEvent(e);
    } else {
        for (View view: childs) {
            consumed = view.dispatchTouchEvent(e);
            if (consumed) {
                break;
            }
        }
        if (!consumed) {
            consumed = onTouchEvent(e);
        }
    }   
    return consumed;
}

這是事件分發(fā)過程的簡單描述,具體遠(yuǎn)比這復(fù)雜的多。

1.3. 核心行為

View 或 ViewGroup 有兩個(gè)核心的行為:攔截(intercept)消費(fèi)(consume)。這兩者是相互獨(dú)立的,攔截不一定消費(fèi)。是否要攔截看 onIntercepTouchEvent。是否要消費(fèi)看 onTouchEvent

注意:是否攔截還有其他因素影響。如果不是 down 事件,并且 mFirstTouchTarget 為空值,就會(huì)直接攔截事件。

dispatchTouchEvent 中有這樣的代碼

攔截的關(guān)鍵源碼

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

從上面的源碼可以看出,在不是 down 事件,并且 mFirstTouchTarget 為空的情況下,不會(huì)走 onInterceptTouchEvent 而是直接攔截。如果滿足了,還會(huì)看 FLAG_DISALLOW_INTERCEPT 標(biāo)記,如果不允許攔截(disallowIntercept 為 true),也不會(huì)走 onInterceptTouchEvent,直接標(biāo)記不攔截。

處理調(diào)用 onTouchEvent 的源碼

boolean result = false;

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

可以看出,在該 View 為 ENABLE 的狀態(tài)并且有 mTouchListener,會(huì)先調(diào)用 onTouch。在 onTouch 返回 false 時(shí)才會(huì)繼續(xù)調(diào)用 onTouchEvent。

onTouch 或者 onTouchEvent 的處理結(jié)果有:

  • 返回 true,會(huì)繼續(xù)消費(fèi)后續(xù)事件。意味著,后面的事件將會(huì)繼續(xù)傳遞到該 View 的 dispatchTouchEvent 方法中進(jìn)行調(diào)度。父 View 會(huì)為該 View 創(chuàng)建一個(gè) TouchTarget 實(shí)例加入鏈表中,鏈表的第一項(xiàng)為 mFirstTouchTarget。后續(xù)的 move 和 up 事件會(huì)直接交給該 View 的 dispatchTouchEvent。
  • 返回 false,不再消費(fèi)后續(xù)事件。意味著,后面的事件將會(huì)被父 View 攔截,而不再傳遞下來。

1.4. 特殊情況

比較特殊的情況有,子 View 可以使用 requestDisallowInterceptTouchEvent 影響去父 View 的分發(fā),可以決定父 View 是否要調(diào)用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用調(diào)用 onInterceptTouchEvent 來判斷攔截,而就是不攔截。

該方法可以用來解決手勢(shì)沖突。比如子 View 先消費(fèi)了事件,但是后面父 View 也滿足了手勢(shì)觸發(fā)的條件而攔截事件,導(dǎo)致子 View 手勢(shì)執(zhí)行一半后無法繼續(xù)響應(yīng)??梢允褂?requestDisallowInterceptTouchEvent(true),這樣后面的事件,父 View 不會(huì)走 onInterceptTouchEvent 回調(diào)來判斷是否要攔截事件,而是直接把事件繼續(xù)傳下來。

2. 案例分析

下面舉三個(gè)簡單的例子,三個(gè)類 ViewGroup1st,ViewGroup2nd 和 View0,層級(jí)關(guān)系為

<ViewGroup1st>
    <ViewGroup2nd>
        <View0 />
    </ViewGroup2nd>
</ViewGroup1st>

這三個(gè)類有兩層 ViewGroup,最底層為 View,這幾個(gè)例子主要理解 消費(fèi) 行為,所以不做事件的攔截。

2.1. 案例1:均不消費(fèi) down 事件

在觸摸屏幕中 View0 的區(qū)域后,輸出 log 信息如下

12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:false

當(dāng) down 事件從 DecorView 開始了分發(fā)過程:

ViewGroup1st 收到事件,執(zhí)行 onInterceptTouchEvent 返回 false,不攔截,于是調(diào)用 ViewGroup2nd 的 dispatchTouchEvent 向 ViewGroup2nd分發(fā)。

ViewGroup2nd 收到事件,dispatchTouchEvent 重復(fù) ViewGroup1st 的分發(fā)策略。因?yàn)槎疾粩r截,所以調(diào)用了 View0 的 dispatchTouchEvent。

View0 收到事件,而 View0 不是 ViewGroup 類型,所以把事件直接交給了 onTouchEvent。

View0 不消費(fèi)事件,onTouchEvent 返回 false,dispatchTouchEvent 方法因此也返回 false。

ViewGroup2nd 因?yàn)?View0 的 dispatchTouchEvent 返回 false,確定了子類不消費(fèi)事件,于是把事件傳遞給 onTouchEvent。但本身也不消費(fèi)事件,所以 onTouchEvent 也返回 false,繼續(xù)把事件上拋到 ViewGroup1st。

ViewGroup1st 重復(fù)了 ViewGroup2nd 的過程。

隨后,move 事件不會(huì)再往下傳了,而是直接被 Activity 攔截。

2.2. 案例2:View0 消費(fèi) down 事件

首先是 down 事件的傳遞,log 如下

12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

ViewGroup1st 和 ViewGroup2st 的傳遞和案例1一樣。區(qū)別在于 View0 onTouchEvent 返回 true 消費(fèi)后續(xù)事件后,View0 的 dispatchTouchEvent 也返回 true,ViewGroup2nd 和 ViewGroup1st 不執(zhí)行 onTouchEvent 也直接返回 true

然后稍微移動(dòng)一下手指,move 事件往下傳遞

12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

過程和 down 事件的傳遞一樣。因?yàn)橥瑯訒?huì)經(jīng)過 ViewGroup2nd 的 onInterceptTouchEvent,如果這時(shí)候 ViewGroup2nd 有攔截行為,move 事件就不會(huì)傳到 View0 了。要避免這種情況發(fā)生,需要調(diào)用 View0 的requestDisallowInterceptTouchEvent,可見 1.4 部分。

2.3. 案例3:ViewGroup2nd 消費(fèi) down 事件

首先是 down 事件的傳遞,log 如下

12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: onTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

由于 View0 不消費(fèi)事件,dispatchTouchEvent 返回 false,所以執(zhí)行了 ViewGroup2nd 的 onTouchEvent 方法。

ViewGroup2nd 消費(fèi)事件,onTouchEvent 返回 true,之后 ViewGroup2nd 和 ViewGroup1st 的 dispatchTouchEvent 均返回 true。

動(dòng)一下手指,move 事件接著傳

2-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

這時(shí)候,ViewGroup2nd 直接攔截了 move 事件,不再經(jīng)過 onInterceptTouchEvent,也不再向 View0 分發(fā),而是直接調(diào)用 onTouchEvent 進(jìn)行處理。

3. down 事件分發(fā)圖

在每個(gè) View 都不攔截 down 事件的情況下,down 事件是這樣傳遞的

down 事件的分發(fā)過程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容