網上介紹TouchEvent分發機制的文章很多,可能有的同學看了還是不明白
這里我會結合源碼、畫圖、簡化代碼結構圖、三個人買手機的類比等多個角度全面解釋
其中用三個人買手機的例子做的類比,可以讓你更具象化的直接理解整個流程
開始介紹事件分發機制之前,先簡單介紹下這個TouchEvent是什么
安卓手機的交互,主要就是手指在屏幕上的戳戳滑滑點點
而我們的這些操作其實主要是由三種基本動作組成的:
- 按下down
- 移動move
- 抬起up
安卓中把這個基礎動作叫做TouchEvent
比如
一次點擊就是按下、抬起組成的
一次長按就是按下、等待、抬起組成
一次滑動操作則是,按下、移動、抬起組成
其實除此之外還有多點觸碰,光標操作等動作,這里暫時用不到,不討論
安卓里經常會有多個控件重疊,即ViewGroup包含View的情況
這個時候點擊到子View時,其實也是同時點到ViewGroup這個父控件的,那是把這個點擊事件分給Parent呢還是Child呢?
這里我們就要了解下安卓中的TouchEvent事件分發機制啦
TouchEvent的分發傳遞主要涉及到三個核心方法
- dispatchTouchEvent 分發Touch事件
- onInterceptTouchEvent 攔截Touch事件
- onTouchEvent 處理Touch事件
其中
onInterceptTouch是ViewGroup的方法。View中則沒有該方法
dispatchTouchEvent在View和ViewGroup中有不同的實現,后面會展開介紹
那么在多層結構中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_DOWN
grandpa 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的
好了,先總結一下
-
dispatchTouchEvent方法內容里處理的是分發過程。可以理解為從A->B->C一層層分發的動作
dispatchTouchEvent的返回值則代表是否將事件分發出去用掉了,自己用或者給某一層子級用都算分發成功。比如B把券用了,或者他發出去給的C把券用了,這兩種情況下B的dispatchTouchEvent都會返回true給A - onInterceptTouchEvent會在第一輪從父到子的時候在分發時調用,以它去決定是否攔截掉此事件不再向下分發。如果攔截下來,就會調用自己的onTouchEvent處理;如果不攔截,則繼續向下傳遞
-
onTouchEvent代表消費掉事件。方法內容是具體的事件處理方法,如何處理點擊滑動等。
onTouchEvent的返回值則代表對上級的反饋,通知這個東西我用掉啦,然后他的父級就會讓分發方法也返回true
舉了這個例子主要是為了說明分發、攔截、消費的流程,可以更具象化的理解,
這樣我們再去用它去解釋為什么例一、二中沒有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_DOWN
grandpa 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可能有多個朋友B1、B2、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代碼~
https://github.com/boredream