網上介紹TouchEvent分發機制的文章不少,可能有的同窗看了仍是不明白
這裏我會結合源碼、畫圖、簡化代碼結構圖、三我的買手機的類比等多個角度全面解釋
其中用三我的買手機的例子作的類比,可讓你更具象化的直接理解整個流程java
開始介紹事件分發機制以前,先簡單介紹下這個TouchEvent是什麼git
安卓手機的交互,主要就是手指在屏幕上的戳戳滑滑點點
而咱們的這些操做其實主要是由三種基本動做組成的:github
安卓中把這個基礎動做叫作TouchEventiphone
好比
一次點擊就是按下、擡起組成的
一次長按就是按下、等待、擡起組成
一次滑動操做則是,按下、移動、擡起組成 ide
其實除此以外還有多點觸碰,光標操做等動做,這裏暫時用不到,不討論源碼分析
安卓裏常常會有多個控件重疊,即ViewGroup包含View的狀況
這個時候點擊到子View時,其實也是同時點到ViewGroup這個父控件的,那是把這個點擊事件分給Parent呢仍是Child呢?
這裏咱們就要了解下安卓中的TouchEvent事件分發機制啦佈局
TouchEvent的分發傳遞主要涉及到三個核心方法this
其中
onInterceptTouch是ViewGroup的方法。View中則沒有該方法
dispatchTouchEvent在View和ViewGroup中有不一樣的實現,後面會展開介紹spa
那麼在多層結構中TouchEvent到底怎麼傳遞呢?
這仨方法用處和調用順序是什麼呢?日誌
下面咱們來擼個Demo實踐下~
【例一】
倆ViewGroup和一個View,方法所有默認不修改~
則當點擊到Child上時,Touch事件的相關方法調用順序就是
grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN
爲何是這樣一個從父級到子級再到父級的U型順序呢?
其實看源碼就知道啦,核心在於ViewGroup的dispatchTouchEvent方法
爲了方便理解,咱們縮減下代碼,以下
boolean dispatchTouchEvent() {
// 是否攔截
boolean intercepted = onInterceptTouchEvent();
if(!intercepted) {
// 若是不攔截遍歷全部child,判斷是否有分發
boolean handled;
if (child == null) {
// 等同於handled = onTouchEvent()
handled = super.dispatchTouchEvent();
} else {
// 若是有child,再調用child的分發方法
handled = child.dispatchTouchEvent();
}
if(handled) {
touchTarget = child;
break;
}
}
if(touchTarget == null) {
// 若是全部child中都沒有消費掉事件
// 那麼就把本身做爲沒child的普通View
handled = super.dispatchTouchEvent();
}
return handled;
}複製代碼
方法的做用是將屏幕點擊事件向下(子一級)傳遞到目標控件上,或者傳遞給本身,若是本身就是目標的話
若是事件被(本身或者下面某一層的子控件)處理掉了的話,就返回true,不然返回false
那問題來了,若是我沒有child了,或者我就是一個View,那個人dispatchTouchEvent返回值要如何獲取呢?
這種狀況下就會使用父類的dispatchTouchEvent方法,
也就是調用View類中的實現,簡化代碼以下
boolean dispatchTouchEvent() {
// 實質上就是調用onTouchEvent用其返回值
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;
}複製代碼
因而可知,只要是enable=false或者沒有設置過touchListener, 那麼他必定會調用onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值
這樣看源碼可能仍是不太理解U型順序
那咱們把代碼也按照上面的三層結構嵌套起來,就很好理解了,以下
其中super.dispatchTouchEvent實際上就是調用了onTouchEvent方法,同時使用其返回值~
經過上圖上的源碼執行順序就知道爲何日誌會這樣輸出了
- grandpa dispatchTouchEvent ACTION_DOWN
- grandpa onInterceptTouchEvent ACTION_DOWN
- --- parent dispatchTouchEvent ACTION_DOWN
- --- parent onInterceptTouchEvent ACTION_DOWN
- --- --- child dispatchTouchEvent ACTION_DOWN
- --- --- child onTouchEvent ACTION_DOWN
- --- parent onTouchEvent ACTION_DOWN
- grandpa onTouchEvent ACTION_DOWN
dispatchTouchEvent分發的方法咱們大概瞭解了,
那onInterceptTouchEvent攔截方法是作什麼用的呢?
該方法用於攔截事件向下分發
當返回值爲true時,就會攔截TouchEvent再也不向下傳遞,直接交給本身的onTouchEvent方法處理。返回false則不攔截。
再作個試驗
【例二】
把例一中的Parent層的onInterceptTouchEvent返回值改成true。
運行一下,點View,看下輸出結果:
grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN
即當事件一層層向下傳遞到parent時,被他就攔截了下來而後本身消費使用。
再看一下源碼中的執行順序原理,以下圖
intercepted爲true~ 沒有進入條件,也就是圖片裏X的地方~
就跳過了child.dispatchTouchEvent的向下事件分發了
最後還剩個onTouchEvent方法
方法的主體內容實際上是處理具體操做邏輯的,是產生一次點擊仍是一次橫縱向的滑動等
而他的返回值纔會影響整個事件分發機制,
其意義在於通知父級的ViewGroup們是否已經消費找到目標Target了
一樣,再試驗一下
【例三】
只把例一中的Parent的TouchEvent返回值改成true。攔截方法不變
點一下View,則輸出日誌爲
grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWNgrandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
--- parent dispatchTouchEvent ACTION_UP
--- parent onTouchEvent ACTION_UP
暫時先看Down的邏輯,對應的源碼執行順序以下
Down部分和例一的前7步流程都是同樣的
可是例三源碼圖片中7的地方,
parent調用super.dispatchTouchEvent其實是調用了onTouchEvent方法,
這裏由於咱們修改爲了true,因此dispatchTouchEvent最終也返回true。
因此返回到grandpa中,touchTarget 就非空了,
所以grandpa的onTouchEvent也沒有執行~
Up部分咱們後面再解釋~
到這裏咱們就能夠看出來
事件一旦被某一層消費掉,其它層就不會再消費了
好了,到這裏其實對事件分發的機制就有個大概瞭解了
看了源碼也知道里面的原理是怎麼回事
可是
爲何例一二中沒有Up,而例三中有呢?
爲何Up和Down的順序不一樣呢?
爲何順序是這樣一個U型的呢?
看的我雲裏霧裏的,光看源碼和簡單的demo仍是太抽象了啊
爲了方便理解,咱們先來個具體事件的類比
事件的消費,就相似咱們用了一個機會券,而後用它去買了一個手機
而事件的傳遞,就相似於這個機會券在不一樣朋友直接的流通傳遞
下面開始描述下這個傳遞的具體過程
有三我的ABC,之間的關係是A和B認識,B和C認識,但A和C不認識
某天A接到別人給它的一張購買iphone8的機會券,用它纔有資格買手機
拿例一作比較對象,下面開始整個類比流程~
A首先接到了這個信息,而後準備開始思考下這個劵的歸屬
(grandpa調用dispatchTouchEvent開始分發)A先想了一下是交給其餘人呢?仍是本身先用掉這個劵呢
(grandpa調用onInterceptTouchEvent判斷是否攔截)A尋思暫時不攔截了吧,而後把劵給了B,讓他去處理下這張劵
(grandpa不攔截,調用child.dispatchTouchEvent)B拿到劵後第一反應也是,我要本身用仍是問有沒有朋友要呢?
(parent調用onInterceptTouchEvent判斷是否攔截)B也有點糾結,算了先問問有沒有其餘朋友要用吧,就給了C
(parent不攔截,調用child.dispatchTouchEvent給C分發)C拿到劵,額我沒朋友,那就不問誰了,那我本身要不要用呢?
不用了最新窮~消費不起,那還給B吧。
(child的分發就是看本身消費與否,返回false給B)B一看,不要啊~ 那我本身要不要消費呢?仍是不了,還給A吧
(parent調用super.dispatchTouchEvent,返回false給A)A拿回了轉了一圈的劵,我手機也沒壞啊也不買了~
(grandpa調用super.dispatchTouchEvent,返回false)
上面就是例一中1~8步驟的狀況,因此最終輸出的日誌就是
grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN
全部人都不消費劵,沒分發出去。
其中步驟6 7 8中都調用了super.dispatchTouchEvent方法,上面咱們介紹過,
這個方法內部其實是調用的onTouchEvent方法~
因此最後的輸出日誌順序就是從父到子依次調用分發和攔截,而後從子到父依次調用消費。
而例二也是同理,區別在於
當B拿到券的時候,選擇了攔截下來再也不詢問其餘朋友了,
可是B又發現本身比較窮,因此也沒消費,直接又還回給了A,
A一樣也不想要新手機也沒有消費這個劵~
因此最終的順序就是,從A到B再返回A就結束了,沒有通過C
例三的狀況就不太同樣了
當A->B->C傳遞到C時,C不消費又返回給了B,B一想別浪費了吧,決定消費掉了劵~
至關於B這個parent調用了onTouchEvent消費方法,返回了true也就是用掉了它,
而後反饋給A說那個券我用了,就等於parent.dispatchTouchEvent返回true給上一級的A了,
A聽到消息後哦了一下~都用掉了,那本身也不用再去考慮用不用的事了
也就是A不會再調用grandpa.onTouchEvent方法了
到這裏再回頭看dispatchTouchEvent返回值的做用就更明確了
它的返回值實際上是用於標誌這個事件是否「用掉了」,
不管是我本身或者下面的子一級用掉了都算是用掉~
再好比這個例子中,若是咱們讓C消費掉事件,
那麼B收到C的消息後,也會調用parent.dispatchTouchEvent返回true給A,
因此這個方法返回值的true是隻要用掉就行,不管本身仍是下面某一級,
而非我把事件傳遞下去就是true了,下面沒人用最終其實仍是返回false的
好了,先總結一下
舉了這個例子主要是爲了說明分發、攔截、消費的流程,能夠更具象化的理解,
這樣咱們再去用它去解釋爲何例1、二中沒有Up,而例三中有就更容易了
仍是作個類比
咱們的這個買手機實際上是一套流程,用券以後還要支付餘下的費用~
用券只是第一步,相似於Down
而支付餘下的費用就相似於Up
結合到一塊兒纔是一個完整的行爲
相似於一個Down+一個Up纔是一次完整的點擊
前倆例子裏爲何沒有Up呢,很好理解,
機會券啊!我都沒用券呢沒購買資格啊,有錢也沒用啊!!!
因此例一二中既然沒人用券,那天然也就不用考慮後續的購買行爲了,所以只有Down,沒Up
而一旦有人消費了,那後續的事件也就會來了
好,咱們拿例三作類比,B消費掉了這個券
那麼如今第二輪來了,銷售員帶着手機先跑來找A,據說有人要買是誰是誰~
這個流程依然是先從A開始分配
(grandpa.dispatchTouchEvent)A這個時候其實還能夠不告訴銷售員誰買的~
(grandpa.onInterceptTouchEvent 判斷是否攔截)可是A仍是沒攔下來,告訴銷售員是B買的
(grandpa不攔截,而後調用child.dispatchTouchEvent)銷售員找到了B,B說沒誰了,就是我了
(parent沒有調用攔截方法)
而後B付錢結帳尾款,完成了整個行爲
(parent調用onTouchEvent返回true消費掉事件)
因此在例三中的Up順序就是
grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
--- parent dispatchTouchEvent ACTION_UP
--- parent onTouchEvent ACTION_UP
此次有了目標,因此不用再來個U型循環了,直接定位到目標B而後結束~
那麼這個目標是怎麼個處理機制呢,咱們會在後面詳細解釋~
回到例三,其實這裏有個地方能夠作點手腳的
就是在售貨員上門找A的時候,A能夠不告訴售貨員B在哪~攔截下來
此次咱們在例三的基礎上進行修改,再整個試驗
【例四】
在grandpa類的onInterceptTouchEvent中添加個判斷,
若是動做是UP就return true攔截掉,DOWN則不攔截和以前同樣
run下代碼,看下輸出日誌
grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWNgrandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP--- parent dispatchTouchEvent ACTION_CANCEL
--- parent onTouchEvent ACTION_CANCEL
前面Down行爲和例三同樣,後面就不一樣了
UP流程變了,而後多了個CANCEL的動做
這裏咱們能夠理解爲
售貨員找到A問誰用的劵啊
(grandpa調用dispatchTouchEvent分發UP事件)A說我不告訴你!你就留我這吧!我得不到的(沒券沒資格買)別人也別想獲得!!!
(grandpa調用onInterceptTouchEvent返回true,攔截UP)而後A告訴B,別等了孫砸!你的券沒用啦!!!!
(parent調用dispatchTouchEvent分發CANCEL動做)而後B也不用再考慮是否消費了,劵丟了吧~
(parent使用CANCEL動做調用onTouchEvent方法,結束)
固然,通常某層要用到事件時都會第一輪向下分發就攔截下來,而後用掉
因此例子三的狀況比較少,不會那麼無私的先問完全部朋友再考慮本身
而例四的狀況也比較少,你要不用就一直不用,要用就直接攔截使用,
通常不會開始說不用~ 後來第二輪的時候又攔腰一刀你們一塊兒死吧!!!的這麼賤~
到這裏其實大概也就瞭解的差很少了,還剩一個TouchTarget目標的概念,
爲何例三中Up和Down流程不一樣?
咱們再回頭去看完整點的源碼~ 此次雖然也是省略代碼,可是比以前的完善點
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 1.每次起始動做就重置以前的TouchTarget等參數
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 2.若是是起始動做才攔截,或者已經有人消費掉了事件,再去判斷攔截
// 起始動做是第一次向下分發的時候,每一個view均可以決定是否攔截,而後進一步判斷是否消費,很好理解
// 若是有人消費掉了事件,那麼也攔截~ 就像例四中的狀況,也能夠再次判斷是否攔截的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 3.這裏能夠設置一個disallowIntercept標誌,若是是true,就是誰收到事件後都不許攔截!!!
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 4.若是未攔截,只有Down動做纔去子一級去找目標對象~
// 由於找目標這個操做只有Down中才會處理
if (actionMasked == MotionEvent.ACTION_DOWN ) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
newTouchTarget = getTouchTarget(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
if (mFirstTouchTarget == null) {
// 5.把本身當作目標,去判斷本身的onTouchEvent是否消費
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 6.若是有人消費掉了事件,找出他~
TouchTarget target = mFirstTouchTarget;
while (target != null) {
// 7.消費對象信息實際上是一個鏈式對象,記載着一個一個傳遞的人的信息,遍歷調用它child的分發方法
final TouchTarget next = target.next;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
target = next;
}
}
}
return handled;
}複製代碼
注意,有一個dispatchTransformedTouchEvent方法,內部簡化代碼爲
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}複製代碼
其實就是判斷若是沒child了(是ViewGroup可是沒子控件,或者本身就是View),
若是沒child,就調用View的dispatchTouchEvent方法,
實質就是調用onTouchEvent判斷是否消費掉事件
若是有child,就調用child的dispatchTouchEvent將事件一層層向下分發
例一二其實只用看以前的最簡化源碼就理解了~
咱們這裏用這個比較完善的源碼分析解釋例三四中的複雜狀況
其中關鍵主要在於多了一個TouchTarget的處理
其實咱們在處理事件的時候,會在第一輪Down的時候先定位到目標,是誰消費了
而後在後續的Move、Up中,利用以前定位的信息更方便的找到目標,直接處理
從上面的源碼中註釋2代碼的位置咱們能夠看出來,
第一次Down的時候咱們纔會去判斷是否攔截,或者有目標的時候才攔截
由於第一次傳券的時候能夠攔截,而若是沒人用券也就是沒有目標那第二輪就不用攔截了,都買不了手機
若是有人消費呢,好比例三中parent消費掉了事件
那麼上面源碼就會在Down時,進入到註釋4代碼的位置,去child一層層找到目標,
當找到某層onTouchEvent返回true消費掉事件的對象後,就會調用addTouchTarget記錄下這個目標
那麼第二輪UP到來時,就會進入註釋2代碼條件,再判斷是否攔截,例三中是不作攔截
再往下運行,由於不是Down,因此不會進入註釋4代碼的判斷條件
到最後,就會在註釋5和6代碼中二選一,例三裏是B消費了,有目標,因此進入條件6,
而後在註釋7代碼處用dispatchTransformedTouchEvent方法,將Up直接向下層層傳遞給目標
向下傳遞的核心主要是在於dispatchTransformedTouchEvent方法
第一輪動做的Down時,只要不攔截,就會在註釋4代碼處遍歷全部child調用該方法層層傳遞下去
然後續其餘動做時,就會進入註釋6代碼條件,而後遍歷TouchTarget中的信息用該方法層層分發
可是要注意不要誤解
第一次Down的時候會for循環全部child,由於A可能有多個朋友B一、B二、B3。。。他會挨個問誰要券啊~
因此第二輪Up的時候也會while(target.next)的迭代循環挨個判斷~可是next是遍歷同級,不是子級
dispatchTrancformTouchEvent(target.child)這裏的.child纔是向子一級一層一層分發傳遞的地方
這個TouchTarget對象,主要保存的是傳遞路線信息,它是一個鏈式結構
不過這個路線不是A->B->C的一個單子,而是ABC每一個人都會保存一個向下的路線信息
好比例子三中B用了券,反饋給了A~ 那麼A這裏就會保存一個A->B的信息,就是從我這裏去找目標B
若是把例一中修改爲C消費掉事件,那麼A就會保存一個A->B,而後B中還會保存一個B->C的信息,
這樣銷售員來找A的時候,若是A不攔截,就會順着A->B的信息找到B,再順着B手裏的B->C信息找到C
當找到最後一個對象的時候,發現C手裏沒有下一個目標的路線信息了,那你就是目標沒跑了~
Cancel部分就不解釋了,dispatchTrancformTouchEvent中會判斷,若是cancel=true動做,
則會把動做改爲ACTION_CANCEL一層一層的傳下去~
其餘還有一些不攔截標誌、id什麼的設置細節就不介紹了,下面能夠本身閱讀下源碼鞏固完善下,
固然我暫時也沒達到每一行代碼都徹底掌握的地步,若是文章有不合適的地方歡迎指正和共同討論~
最後宣傳一下我的的Github帳號,有多個不錯的開源項目喲~
歡迎follow我和star代碼~
github.com/boredream