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

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

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

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

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

1.1. 概述

事件主要有 down (MotionEvent.ACTION_DOWN)土铺,moveMotionEvent.ACTION_MOVE)后德,upMotionEvent.ACTION_UP)。
基本上的手勢均由 down 事件為起點,up 事件為終點拷况,中間可能會有一定數(shù)量的 move 事件起意。這三種事件是大部分手勢動作的基礎(chǔ)套菜。

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

大體的分發(fā)過程為:首先傳遞到 Activity戏溺,然后傳給了 Activity 依附的 Window渣蜗,接著由 Window 傳給視圖的頂層 View 也就是 DecorView,最后由 DecorView 向整個 ViewTree 分發(fā)于购。分發(fā)還會有回溯的過程肋僧。最后還會回到 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 中。如果整個 ViewTree 沒有消費事件,會調(diào)用 Activity 的 onTouchEvent斥扛。

1.2. 主要方法

1.2.1. 概覽

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

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

onInterceptTouchEvent該方法決定了是否攔截事件。只有 ViewGroup 有該回調(diào)设捐。返回 true 表示攔截,返回 false 表示不攔截。自定義 View 的時候杂数,可以重載該方法诉探,通過一些特定的邏輯來決定是否攔截事件憎亚。如果攔截科侈,接下來會調(diào)用該 ViewGroup 的 onTouchEvent 來處理事件如孝。

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

  • 該 View 攔截了事件
  • 子 View 都不消費事件
  • 沒有子 View

該方法正式處理 MotionEvent榔幸。返回 true 表示消費趟咆,返回 false 不消費。如果消費,接下來的事件還會傳遞到該 View 的 dispatchTouchEvent 中;如果不消費抢韭,后面的事件不會再傳過來呛凶。

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

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

大部分手勢的起點為 down 事件糙麦,dispatchTouchEvent 如果收到 down 事件辛孵,會重新設(shè)置一些變量和標記

重置變量和標記的源碼

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

實際的源碼中,ViewGroup 繼承于 View赡磅。 當子 View 不消費事件或者 ViewGroup 攔截了事件會傳空值到 dispatchTransformedTouchEvent 中魄缚,內(nèi)部會調(diào)用 super.dispatchTouchEvent,最終把事件傳給 onTouchEvent 進行處理焚廊。

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)化坐標為 child 的坐標系,調(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>

對于一個 ViewGroup 來說嚼隘,幾個重要方法的關(guān)系如下

幾個重要方法關(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ā)過程的簡單描述,具體遠比這復(fù)雜的多袒餐。

1.3. 核心行為

View 或 ViewGroup 有兩個核心的行為:攔截(intercept)消費(consume)嗓蘑。這兩者是相互獨立的,攔截不一定消費匿乃。是否要攔截看 onIntercepTouchEvent桩皿。是否要消費看 onTouchEvent

注意:是否攔截還有其他因素影響幢炸。如果不是 down 事件泄隔,并且 mFirstTouchTarget 為空值,就會直接攔截事件宛徊。

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 為空的情況下,不會走 onInterceptTouchEvent 而是直接攔截暖呕。如果滿足了斜做,還會看 FLAG_DISALLOW_INTERCEPT 標記,如果不允許攔截(disallowIntercept 為 true)湾揽,也不會走 onInterceptTouchEvent瓤逼,直接標記不攔截。

處理調(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霸旗,會先調(diào)用 onTouch。在 onTouch 返回 false 時才會繼續(xù)調(diào)用 onTouchEvent戚揭。

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

  • 返回 true诱告,會繼續(xù)消費后續(xù)事件。意味著民晒,后面的事件將會繼續(xù)傳遞到該 View 的 dispatchTouchEvent 方法中進行調(diào)度精居。父 View 會為該 View 創(chuàng)建一個 TouchTarget 實例加入鏈表中,鏈表的第一項為 mFirstTouchTarget潜必。后續(xù)的 move 和 up 事件會直接交給該 View 的 dispatchTouchEvent箱蟆。
  • 返回 false,不再消費后續(xù)事件刮便。意味著,后面的事件將會被父 View 攔截绽慈,而不再傳遞下來恨旱。

1.4. 特殊情況

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

該方法可以用來解決手勢沖突耕陷。比如子 View 先消費了事件掂名,但是后面父 View 也滿足了手勢觸發(fā)的條件而攔截事件,導(dǎo)致子 View 手勢執(zhí)行一半后無法繼續(xù)響應(yīng)哟沫〗让铮可以使用 requestDisallowInterceptTouchEvent(true),這樣后面的事件嗜诀,父 View 不會走 onInterceptTouchEvent 回調(diào)來判斷是否要攔截事件猾警,而是直接把事件繼續(xù)傳下來孔祸。

2. 案例分析

下面舉三個簡單的例子,三個類 ViewGroup1st发皿,ViewGroup2nd 和 View0崔慧,層級關(guān)系為

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

這三個類有兩層 ViewGroup,最底層為 View穴墅,這幾個例子主要理解 消費 行為惶室,所以不做事件的攔截。

2.1. 案例1:均不消費 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

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

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

ViewGroup2nd 收到事件,dispatchTouchEvent 重復(fù) ViewGroup1st 的分發(fā)策略惩坑。因為都不攔截掉盅,所以調(diào)用了 View0 的 dispatchTouchEvent

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

View0 不消費事件蔓钟,onTouchEvent 返回 false永票,dispatchTouchEvent 方法因此也返回 false。

ViewGroup2nd 因為 View0 的 dispatchTouchEvent 返回 false滥沫,確定了子類不消費事件侣集,于是把事件傳遞給 onTouchEvent。但本身也不消費事件兰绣,所以 onTouchEvent 也返回 false世分,繼續(xù)把事件上拋到 ViewGroup1st。

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

隨后臭埋,move 事件不會再往下傳了,而是直接被 Activity 攔截臀玄。

2.2. 案例2:View0 消費 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 消費后續(xù)事件后健无,View0 的 dispatchTouchEvent 也返回 true炫掐,ViewGroup2nd 和 ViewGroup1st 不執(zhí)行 onTouchEvent 也直接返回 true

然后稍微移動一下手指,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 事件的傳遞一樣睬涧。因為同樣會經(jīng)過 ViewGroup2nd 的 onInterceptTouchEvent募胃,如果這時候 ViewGroup2nd 有攔截行為旗唁,move 事件就不會傳到 View0 了。要避免這種情況發(fā)生痹束,需要調(diào)用 View0 的requestDisallowInterceptTouchEvent检疫,可見 1.4 部分。

2.3. 案例3:ViewGroup2nd 消費 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 不消費事件屎媳,dispatchTouchEvent 返回 false,所以執(zhí)行了 ViewGroup2nd 的 onTouchEvent 方法论巍。

ViewGroup2nd 消費事件烛谊,onTouchEvent 返回 true,之后 ViewGroup2nd 和 ViewGroup1st 的 dispatchTouchEvent 均返回 true嘉汰。

動一下手指丹禀,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

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

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

在每個 View 都不攔截 down 事件的情況下焙矛,down 事件是這樣傳遞的

down 事件的分發(fā)過程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市残腌,隨后出現(xiàn)的幾起案子村斟,更是在濱河造成了極大的恐慌,老刑警劉巖抛猫,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蟆盹,死亡現(xiàn)場離奇詭異,居然都是意外死亡邑滨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門钱反,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掖看,“玉大人,你說我怎么就攤上這事面哥“タ牵” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵尚卫,是天一觀的道長归榕。 經(jīng)常有香客問我,道長吱涉,這世上最難降的妖魔是什么刹泄? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任外里,我火速辦了婚禮,結(jié)果婚禮上特石,老公的妹妹穿的比我還像新娘盅蝗。我一直安慰自己,他們只是感情好姆蘸,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布墩莫。 她就那樣靜靜地躺著,像睡著了一般逞敷。 火紅的嫁衣襯著肌膚如雪狂秦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天推捐,我揣著相機與錄音裂问,去河邊找鬼。 笑死玖姑,一個胖子當著我的面吹牛愕秫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焰络,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼戴甩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闪彼?” 一聲冷哼從身側(cè)響起甜孤,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎畏腕,沒想到半個月后缴川,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡描馅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年把夸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铭污。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡恋日,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嘹狞,到底是詐尸還是另有隱情岂膳,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布磅网,位于F島的核電站谈截,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜簸喂,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一毙死、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娘赴,春花似錦规哲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至竿奏,卻和暖如春袄简,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泛啸。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工绿语, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人候址。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓吕粹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岗仑。 傳聞我的和親對象是個殘疾皇子匹耕,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容