Android事件分發機制二:viewGroup與view對事件的處理
阿新 • • 發佈:2021-01-23
## 前言
很高興遇見你~
在上一篇文章 [Android事件分發機制一:事件是如何到達activity的?](https://juejin.cn/post/6918272111152726024) 中,我們討論了觸控資訊從螢幕產生到傳送給具體 的view處理的整體流程,這裡先來簡單回顧一下:
![整體流程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac07f9a992324f0ea6dfe6b5f4236a67~tplv-k3u1fbpfcp-zoom-1.image)
1. 觸控資訊從手機觸控式螢幕幕時產生,通過IMS和WMS傳送到viewRootImpl
2. viewRootImpl把觸控資訊傳遞給他所管理的view
3. view根據自身的邏輯對事件進行分發
4. 常見的如Activity佈局的頂層viewGroup為DecorView,他對事件分發方法進行了重新,會優先回調windowCallBack也就是Activity的分發方法
5. 最後事件都會交給viewGroup去分發給子view
前面的分發步驟我們清楚了,那麼viewGroup是如何對觸控事件進行分發的呢?View又是如何處理觸控資訊的呢?正是本文要討論的內容。
事件處理中涉及到的關鍵方法就是 `dispatchTouchEvent` ,不管是viewGroup還是view。在viewGroup中,`dispatchTouchEvent` 方法主要是把事件分發給子view,而在view中,`dispatchTouchEvent` 主要是處理消費事件。而主要的消費事件內容是在 `onTouchEvent` 方法中。下面討論的是viewGroup與view的預設實現,而在自定義view中,通常會重寫 `dispatchTouchEvent` 和 `onTouchEvent` 方法,例如DecorView等。
秉著邏輯先行原始碼後到的原則,本文雖然涉及到大量的原始碼,但會優先講清楚流程,有時間的讀者仍然建議閱讀完整原始碼。
## 理解MotionEvent
事件分發中涉及到一個很重要的點:多點觸控,這是在很多的文章中沒有體現出來的。而要理解viewGroup如何處理多點觸控,首先需要對觸控事件資訊類:MotionEvent,有一定的認識。MotionEvent中承載了觸控事件的很多資訊,理解它更有利於我們理解viewGroup的分發邏輯。所以,首先需要先理解MotionEvent。
觸控事件的基本型別有三種:
- ACTION_DOWN: 表示手指按下螢幕
- ACTION_MOVE: 手指在螢幕上滑動時,會產生一系列的MOVE事件
- ACTION_UP: 手指抬起,離開螢幕
**一個完整的觸控事件系列是:從ACTION_DOWN開始,到ACTION_UP結束** 。這其實很好理解,就是手指按下開始,手指抬起結束。
手指可能會在螢幕上滑動,那麼中間會有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP。
這是正常的情況,而如果出現了一些異常的情況,事件序列被中斷,那麼會產生一個取消事件:
- ACTION_CANCEL:當出現異常情況事件序列被中斷,會產生該型別事件
所以,完整的事件序列是:**從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束** 。當然,這是我們一個手指的情況,那麼在多指操作的情況是怎麼樣的呢?這裡需要引入另外的事件型別:
- ACTION_POINTER_DOWN: 當已經有一個手指按下的情況下,另一個手指按下會產生該事件
- ACTION_POINTER_UP: 多個手指同時按下的情況下,抬起其中一個手指會產生該事件
區別於ACTION_DOWN和ACTION_UP,使用另外兩個事件型別來表示手指的按下與抬起,使得**ACTION_DOWN和ACTION_UP可以作為一個完整的事件序列的邊界** 。
**同時,一個手指的事件序列,是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束。**
到這裡先簡單做個小結:
> 觸控事件的型別有:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_DOWN、ACTION_POINTER_UP,他們分別代表不同的場景。
>
> 一個完整的事件序列是從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束。
> **一個手指**的完整序列是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束。
---
第二,我們需要理解MotionEvent中所攜帶的資訊。
假如現在螢幕上有兩個手指按下,如下圖:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a93e3ab6b29548c49bc5b28686eba4a8~tplv-k3u1fbpfcp-zoom-1.image)
觸控點a先按下,而觸控點b**後**按下,那麼自然而然就會產生兩個事件:ACTION_DOWN和ACTION_POINTER_DOWN。那麼是不是ACTION_DOWN事件就只包含有觸控點a的資訊,而ACTION_POINTER_DOWN只包含觸控點b的資訊呢?換句話說,這兩個事件是不是會獨立發出觸控事件?答案是:不是。
每一個觸控事件中,都包含有所有觸控點的資訊。例如上述的點b按下時產生的ACTION_POINTER_DOWN事件中,就包含了觸控點a和觸控點b的資訊。那麼他是如何區分這兩個點的資訊?我們又是如何知道ACTION_POINTER_DOWN這個事件型別是屬於觸控點a還是觸控點b?
在MotionEvent物件內部,維護有一個數組。這個陣列中的每一項對應不同的觸控點的資訊,如下圖:
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2ea91173329439d819540981ae37f45~tplv-k3u1fbpfcp-zoom-1.image)
陣列下標稱為觸控點的索引,每個節點,擁有一個觸控點的完整資訊。這裡要注意的是,一個觸控點的索引並不是一成不變的,而是會隨著觸控點的數目變化而變化。例如當同時按下兩個手指時,陣列情況如下圖:
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dea3347932984b77986556107412ba73~tplv-k3u1fbpfcp-zoom-1.image)
而當手指a抬起後,陣列的情況變為下圖:
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7bb140f2b37e4f569f132b3edce5afc8~tplv-k3u1fbpfcp-zoom-1.image)
可以看到觸控點b的索引改變了。所以**跟蹤一個觸控點必須是依靠一個觸控點的id,而不是他的索引** 。
現在我們知道每一個MotionEvent內部都維護有所有觸控點的資訊,那麼我們怎麼知道這個事件是對應哪個觸控點呢?這就需要看到MotionEvent的一個方法:`getAction` 。
這個方法返回一個整型變數,他的低1-8位表示該事件的型別,高9-16位表示觸控點索引。我們只需要將這16位進行分離,就可以知道觸控點的型別和所對應的觸控點。同時,MotionEvent有兩個獲取觸控點座標的方法:`getX()/getY()` ,他們都需要傳入一個觸控點索引來表示獲取哪個觸控點的座標資訊。
同時還要注意的是,MOVE事件和CANCEL事件是沒有包含觸控點索引的,只有DOWN型別和UP型別的事件才包含觸控點索引。這裡是因為非DOWN/UP事件,不涉及到觸控點的增加與刪除。
這裡我們再來小結一下:
> - 一個MotionEvent物件內部使用一個數組來維護所有觸控點的資訊
> - UP/DOWN型別的事件包含了觸控點索引,可以根據該索引做出對應的操作
> - 觸控點的索引是變化的,不能作為跟蹤的依據,而必須依據觸控點id
----
關於MotionEvent需要了解一個更加重要的點:事件分離。
首先需要知道事件分發的一個原則:**一個view消費了某一個觸點的down事件後,該觸點事件序列的後續事件,都由該view消費** 。這也比較符合我們的操作習慣。當我們按下一個控制元件後,只要我們的手指一直沒有離開螢幕,那麼我們希望這個手指滑動的資訊都交給這個view來處理。換句話說,一個觸控點的事件序列,只能給一個view消費。
經過前面的描述我們知道,一個事件是包含所有觸控點的資訊的。當viewGroup在派發事件時,每個觸控點的資訊就需要分開分別傳送給感興趣的view,這就是事件分離。
例如Button1接收了觸控點a的down事件,Button2接收了觸控點b的down事件,那麼當一個MotionEvent物件到來時,需要將他裡面的觸控點資訊,把觸控點a的資訊拆開發送給button1,把觸控點b的資訊拆開發送給button2。如下圖:
![事件分離](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0007635a8ced425ab58bf61f7ab78ae7~tplv-k3u1fbpfcp-zoom-1.image)
那麼,可不可以不進行分離?當然可以。這樣的話每次都把所有觸控點的資訊傳送給子view。這可以通過FLAG_SPLIT_MOTION_EVENTS這個標誌進行設定是否要進行分離。
小結一下:
> 一個觸控點的序列一般情況下只給一個view處理,當一個view消費了一個觸控點的down事件後,該觸控點的事件序列後續事件都會交給他處理。
>
> 事件分離是把一個motionEvent中的觸控點資訊進行分離,只向子view傳送其感興趣的觸控點資訊。
>
> 我們可以通過設定FLAG_SPLIT_MOTION_EVENTS標誌讓viewGroup是否對事件進行分離
---
到這裡關於MotionEvent的內容就講得差不多,當然在分離的時候,還需要進行一定的調整,例如座標軸的更改、事件型別的更改等等,放在後面講,接下來看看ViewGroup是如何分發事件的。
## ViewGroup對於事件的分發
這一步可以說是事件分發中的重頭戲了。不過在理解了上面的MotionEvent之後,對於ViewGroup的分發細節也就容易理解了。
整體來說,ViewGroup分發事件分為三個大部分,後面的內容也會圍繞著三大部分展開:
1. 攔截事件:在一定情況下,viewGroup有權利選擇攔截事件或者交給子view處理
2. 尋找接收事件序列的控制元件:每一個需要分發給子view的down事件都會先尋找是否有適合的子view,讓子view來消費整個事件序列
3. 派發事件:把事件分發到感興趣的子view中或自己處理
大體的流程是:每一個事件viewGroup會先判斷是否要攔截,如果是down事件(這裡的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),還需要挨個遍歷子view看看是否有子view消費了down事件,最後再把事件派發下去。
在開始解析之前,必須先了解一個關鍵物件:TouchTarget。
#### TouchTarget
前面我們講到:一個觸控點的序列一般情況下只給一個view處理,當一個view消費了一個觸控點的down事件後,該觸控點的事件序列後續事件都會交給他處理。對於viewGroup來說,他有很多個子view,如果不同的子view接受了不同的觸控點的down事件,那麼ViewGroup如何記錄這些資訊並精準把事件傳送給對應的子view呢?答案就是:TouchTarget。
TouchTarget中維護了每個子view以及所對應的觸控點id,這裡的id可以不止一個。TouchTarget本身是個連結串列,每個節點記錄了子view所對應的觸控點id。在viewGroup中,該連結串列的連結串列頭是mFirstTouchTarget,如果他為null,表示沒有任何子view接收了down事件。
TouchTarget有個非常神奇的設計,他只使用一個整型變數來記錄所有的觸控id。整型變數中哪一個二進位制位為1,則對應繫結該id的觸控點。
例如 00000000 00000000 00000000 10001000,則表示綁定了id為3和id為7的兩個觸控點,因為第3位和第7位的二進位制位是1。這裡可以間接說明系統支援的最大多點觸控數是32,當然實際上一般是8比較多。當要判斷一個TouchTarget綁定了哪些id時,只需要通過一定的位操作即可,既提高了速度,也優化了空間佔用。
當一個down事件來臨時,viewGroup會為這個down事件尋找適合的子view,併為他們建立一個TouchTarget加入到連結串列中。而當一個up事件來臨時,viewGroup會把對應的TouchTarget節點資訊刪除。那接下來,就直接看到viewGroup中的`dispatchTouchEvent` 是如何分發事件的。首先看到原始碼中的第一部分:事件攔截。
---
#### 事件攔截
這裡的攔截分為兩部分:安全攔截和邏輯攔截。
安全攔截是一直被忽略的一種情況。當一個控制元件a被另一個非全屏控制元件b遮擋住的時候,那麼有可能被惡意軟體操作發生危險。例如我們看到的介面是這樣的:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b766a612077941c0afff0e7eb4515e3a~tplv-k3u1fbpfcp-zoom-1.image)
但實際上,我們看到的這個按鈕時不可點選的,實際上觸控事件會被分發到這個按鈕後面的真正接收事件的按鈕:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fdb1b9bdc5bb4b8cb365accad3fef120~tplv-k3u1fbpfcp-zoom-1.image)
然後我們就白給了。這個安全攔截行為由兩個標誌控制:
- FILTER_TOUCHES_WHEN_OBSCURED:這個標誌可以手動給控制元件設定,表示被非全屏控制元件覆蓋時,直接過濾掉所有觸控事件。
- FLAG_WINDOW_IS_OBSCURED:這個標誌表示當前視窗被一個非全屏控制元件覆蓋。
具體的原始碼如下:
```java
View.java api29
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
// 兩個標誌,前者表示當被覆蓋時不處理;後者表示當前視窗是否被非全屏視窗覆蓋
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
```
第二種攔截是邏輯攔截。如果當前viewGroup中沒有TouchTarget,而且這個事件不是down事件,這就意味著viewGroup自己消費了先前的down事件,那麼這個事件就無須分發到子view必須自己消費,也就不需要攔截這種情況的事件。除此之外的事件都是需要分發到子view,那麼viewGroup就可以對他們進行判斷是否進行攔截。簡單來說,**只有需要分發到子view的事件才需要攔截** 。
判斷是否攔截主要依靠兩個因素:FLAG_DISALLOW_INTERCEPT標誌和 `onInterceptTouchEvent()` 方法。
1. 子view可以通過requestDisallowInterupt方法強制要求viewGroup不要攔截事件,viewGroup中會設定一個FLAG_DISALLOW_INTERCEPT標誌表示不攔截事件。但是當前事件序列結束後,這個標誌會被清除。如果需要的話需要再次呼叫requestDisallowInterupt方法進行設定。
2. 如果子view沒有強制要求不攔截,那麼會呼叫`onInterceptTouchEvent()` 方法判斷是否需要攔截。onInterceptTouchEvent方法預設只對一種特殊情況作了攔截。一般情況下我們會重寫這個方法來攔截事件:
```java
// 只對一種特殊情況做了攔截
// 滑鼠左鍵點選了滑動塊
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
```
viewGroup的 `dispatchTouchEvent` 方法邏輯中對於事件攔截部分的原始碼分析如下:
```java
ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 對遮蓋狀態進行過濾
if (onFilterTouchEventForSecurity(ev)) {
...
// 判斷是否需要攔截
final boolean intercepted;
// down事件或者有target的非down事件則需要判斷是否需要攔截
// 否則不需要進行攔截判斷,因為一定是交給自己處理
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 此標誌為子view通過requestDisallowInterupt方法設定
// 禁止viewGroup攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 呼叫onInterceptTouchEvent判斷是否需要攔截
intercepted = onInterceptTouchEvent(ev);
// 恢復事件狀態
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 自己消費了down事件,那麼後續的事件非down事件都是自己處理
intercepted = true;
}
...;
}
...;
}
```
---
#### 尋找消費down事件的子控制元件
對於每一個down事件,不管是ACTION_DOWN還是ACTION_POINTER_DOWN,viewGroup都會優先在控制元件樹中尋找合適的子控制元件來消費他。因為對於每一個down事件,標誌著一個觸控點的一個嶄新的事件序列,viewGroup會盡自己的最大能力尋找合適的子控制元件。如果找不到合適的子控制元件,才會自己處理down事件。因為,消費了down事件,意味著接下來該觸控點的事件序列事件都會交給該view消費,如果viewGroup攔截了事件,那麼子view就無法接收到任何事件訊息。
viewGroup尋找子控制元件的步驟也不復雜。首先viewGroup會為他的子控制元件構造一個控制元件列表,構造的順序是view的繪製順序的逆序,也就是一個view的z軸系數越高,顯示高度越高,在列表的順序就會越靠前。這其實比較好理解,顯示越高的控制元件肯定是優先接收點選的。除了預設情況,我們也可以進行自定義列表順序,這裡就不展開了。
viewGroup會按順序遍歷整個列表,判斷觸控點的位置是否在該view的範圍內、該view是否可以點選等,尋找合適的子view。如果找到合適的子view,則會把down事件分發給他,如果該view接收事件,則會為他建立一個TouchTarget,將該觸控id和view進行繫結,之後該觸控點的事件就可以直接分發給他了。
而如果沒有一個控制元件適合,那麼會預設選取TouchTarget連結串列的最新一個節點。也就是當我們多點觸控時,兩次手指按下,如果沒有找到合適的子view,那麼就被認為是和上一個手指點選的是同個view。因此,如果viewGroup當前有正在消費事件的子控制元件,那麼viewGroup自己是不會消費down事件的。
接下來我們看看原始碼分析(程式碼有點長,需要慢慢分析理解):
```java
ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 對遮蓋狀態進行過濾
if (onFilterTouchEventForSecurity(ev)) {
// action的高9-16位表示索引值
// 低1-8位表示事件型別
// 只有down或者up事件才有索引值
final int action = ev.getAction();
// 獲取到真正的事件型別
final int actionMasked = action & MotionEvent.ACTION_MASK;
...
// 攔截內容的邏輯
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
}
...
// 三個變數:
// split表示是否需要對事件進行分裂,對應多點觸控事件
// newTouchTarget 如果是down或pointer_down事件的新的繫結target
// alreadyDispatchedToNewTouchTarget 表示事件是否已經分發給targetview了
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果沒有取消和攔截進入分發
if (!canceled && !intercepted) {
...
// down或pointer_down事件,表示新的手指按下了,需要尋找接收事件的view
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 多點觸控會有不同的索引,獲取索引號
// 該索引位於MotionEvent中的一個數組,索引值就是陣列下標值
// 只有up或down事件才會攜帶索引值
final int actionIndex = ev.getActionIndex();
// 這個整型變數記錄了TouchTarget中view所對應的觸控點id
// 觸控點id的範圍是0-31,整型變數中哪一個二進位制位為1,則對應繫結該id的觸控點
// 例如 00000000 00000000 00000000 10001000
// 則表示綁定了id為3和id為7的兩個觸控點
// 這裡根據是否需要分離,對觸控點id進行記錄,
// 而如果不需要分離,則預設接收所有觸控點的事件
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// down事件表示該觸控點事件序列是一個新的序列
// 清除之前繫結到到該觸控id的TouchTarget
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
// 如果子控制元件數目不為0而且還沒繫結到新的id
if (newTouchTarget == null && childrenCount != 0) {
// 使用觸控點索引獲取觸控點位置
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 從前到後建立view列表
final ArrayList preorderedList = buildTouchDispatchChildList();
// 判斷是否是自定義view順序
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);
...
// 檢查該子view是否可以接受觸控事件和是否在點選的範圍內
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 檢查該子view是否在touchTarget連結串列中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 連結串列中已經存在該子view,說明這是一個多點觸控事件
// 即兩次都觸控到同一個view上
// 將新的觸控點id繫結到該TouchTarget上
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
// 如果消費了,需要生成新的TouchTarget
// 如果沒有消費,說明子view不接受該down事件,繼續迴圈尋找合適的子控制元件
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();
// 儲存該view到target連結串列
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 標記已經分發給子view,退出迴圈
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}// 這裡對應for (int i = childrenCount - 1; i > = 0; i--)
...
}// 這裡對應判斷:(newTouchTarget == null && childrenCount != 0)
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 沒有子view接收down事件,直接選擇連結串列尾的view作為target
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}// 這裡對應if (actionMasked == MotionEvent.ACTION_DOWN...)
}// 這裡對應if (!canceled && !intercepted)
...
}// 這裡對應if (onFilterTouchEventForSecurity(ev))
...
}
```
#### 派發事件
經過了攔截與尋找消費down事件的控制元件之後,無論前面的處理結果如何,最終都是需要將事件進行派發,不管是派發給自己還是子控制元件。這裡派發的物件只有兩個:viewGroup自身或TouchTarget。
經過了前面的尋找消費down事件子控制元件步驟,那麼每個觸控點都找到了消費自己事件序列的控制元件並繫結在了TouchTarget中;而如果沒有找到合適的子控制元件,那麼消費的物件就是viewGroup自己。因此派發事件的主要任務就是:**把不同觸控點的資訊分發給合適的viewGroup或touchTarget。**
派發的邏輯需要結合前面MotionEvent和TouchTarget的內容。我們知道MotionEvent包含了當前螢幕所有觸控點資訊,而viewGroup的每個TouchTarget則包含了不同的view所感興趣的觸控點。
如果不需要進行事件分離,那麼直接將當前的所有觸控點的資訊都發送給每個TouchTarget即可;
如果需要進行事件分離,那麼會將MotionEvent中不同觸控點的資訊拆開分別建立新的MotionEvent,併發送給感興趣的子控制元件;
如果TouchTarget連結串列為空,那麼直接分發給viewGroup自己;所以touchTarget不為空的情況下,viewGroup自己是不會消費事件的,這也就意味著viewGroup和其中的view不會同時消費事件。
![事件分離派發事件](https://i.loli.net/2021/01/21/6Myj1AaF2XeRVq9.png)
上圖展示了需要事件分離的情況下進行的事件分發。
在把原MotionEvent拆分成多個MotionEvent時,不僅需要把不同的觸控點資訊進行分離,還需要對座標進行轉換和改變事件型別:
- 我們接收到的觸控點的位置資訊並不是基於螢幕座標系,而是基於當前view的座標系。所以當viewGroup往子view分發事件時,需要把觸控點的資訊轉換成對應view的座標系。
- viewGroup收到的事件型別和子view收到的事件型別並不是完全一致的,在分發給子view的時候,viewGroup需要對事件型別進行修改,一般有以下情況需要修改:
1. viewGroup收到一個ACTION_POINTER_DOWN事件分發給一個子view,但是該子view前面沒有收到其他的down事件,所以對於該view來說這是一個嶄新的事件序列,所以需要把這個ACTION_POINTER_DOWN事件型別改為ACTION_DOWN再發送給子view。
2. viewGroup收到一個ACTION_POINTER_DOWN或ACTION_POINTER_UP事件,假設這個事件型別對應觸控點2,但是有一個子view他只對觸控點1的事件序列感興趣,那麼在分離出觸控點1的資訊之後,還需要把事件型別改為ACTION_MOVE再分發給該子view。
- 注意,把原MotionEvent物件拆分為多個MotionEvent物件之後,觸控點的索引也發生了改變,如果需要分發一個ACTION_POINTER_DOWN/UP事件給子view,那麼需要注意更新觸控點的索引值。
viewGroup中真正執行事件派發的關鍵方法是 `dispatchTransformedTouchEvent` ,該方法會完成關鍵的事件分發邏輯。原始碼分析如下:
```java
ViewGroup.java api29
// 該方法接收原MotionEvent事件、是否進行取消、目標子view、以及目標子view感興趣的觸控id
// 如果不是取消事件這個方法會把原MotionEvent中的觸控點資訊拆分出目標view感興趣的觸控點資訊
// 如果是取消事件則不需要拆分直接傳送取消事件即可
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 如果是取消事件,那麼不需要做其他額外的操作,直接派發事件即可,然後直接返回
// 因為對於取消事件最重要的內容就是事件本身,無需對事件的內容進行設定
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;
}
// oldPointerIdBits表示現在所有的觸控id
// desirePointerIdBits來自於該view所在的touchTarget,表示該view感興趣的觸控點id
// 因為desirePointerIdBits有可能全是1,所以需要和oldPointerIdBits進行位與
// 得到真正可接收的觸控點資訊
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// 控制元件處於不一致的狀態。正在接受事件序列卻沒有一個觸控點id符合
if (newPointerIdBits == 0) {
return false;
}
// 來自原始MotionEvent的新的MotionEvent,只包含目標感興趣的觸控點
// 最終派發的是這個MotionEvent
final MotionEvent transformedEvent;
// 兩者相等,表示該view接受所有的觸控點的事件
// 這個時候transformedEvent相當於原始MotionEvent的複製
if (newPointerIdBits == oldPointerIdBits) {
// 當目標控制元件不存在通過setScaleX()等方法進行的變換時,
// 為了效率會將原始事件簡單地進行控制元件位置與滾動量變換之後
// 傳送給目標的dispatchTouchEvent()方法並返回。
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
// 複製原始MotionEvent
transformedEvent = MotionEvent.obtain(event);
} else {
// 如果兩者不等,說明需要對事件進行拆分
// 只生成目標感興趣的觸控點的資訊
// 這裡分離事件包括了修改事件的型別、觸控點索引等
transformedEvent = event.split(newPointerIdBits);
}
// 對MotionEvent的座標系,轉換為目標控制元件的座標系並進行分發
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// 計算滾動量偏移
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
// 存在scale等變換,需要進行矩陣轉換
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
// 呼叫子view的方法進行分發
handled = child.dispatchTouchEvent(transformedEvent);
}
// 分發完畢,回收MotionEvent
transformedEvent.recycle();
return handled;
}
```
好了,瞭解完上面的內容,來看看viewGroup的 `dispatchTouchEvent` 中派發事件的程式碼部分:
```java
ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 對遮蓋狀態進行過濾
if (onFilterTouchEventForSecurity(ev)) {
...
if (mFirstTouchTarget == null) {
// 經過了前面的處理,到這裡touchTarget依舊為null,說明沒有找到處理down事件的子控制元件
// 或者down事件被viewGroup本身消費了,所以該事件由viewGroup自己處理
// 這裡呼叫了dispatchTransformedTouchEvent方法來分發事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 已經有子view消費了down事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
// 遍歷所有的TouchTarget並把事件分發下去
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 表示事件在前面已經處理了,不需要重複處理
handled = true;
} else {
// 正常分發事件或者分發取消事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 這裡呼叫了dispatchTransformedTouchEvent方法來分發事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果傳送了取消事件,則移除該target
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 如果接收到取消獲取up事件,說明事件序列結束
// 直接刪除所有的TouchTarget
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 清除記錄的資訊
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
// 如果僅僅只是一個PONITER_UP
// 清除對應觸控點的觸控資訊
removePointersFromTouchTargets(idBitsToRemove);
}
}// 這裡對應if (onFilterTouchEventForSecurity(ev))
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
```
#### 小結
到這裡,viewGroup的事件分發原始碼就解析完成了,這裡再來小結一下:
- 每一個觸控點的事件序列,只能給一個view消費;如果一個view消費了一個觸控點的down事件,那麼該觸控點的後續事件都會給他處理。
- 每一個事件到達viewGroup,如果需要分發到子view,那麼viewGroup會新判斷是否要攔截。
- 當viewGroup的touchTarget!=null || 事件的型別為down 需要進行判斷是否攔截;
- 判斷是否攔截受兩個因素影響:onInterceptTouchEvent和FLAG_DISALLOW_INTERCEPT標誌
- 如果該事件是down型別,那麼需要遍歷所有的子控制元件判斷是否有子控制元件消費該down事件
- 當有新的down事件被消費時,viewGroup會把該view和對應的觸控點id繫結起來儲存到touchTarget中
- 根據前面的處理情況,將事件派發到viewGroup自身或touchTarget中
- 如果touchTarget==null,說明沒有子控制元件消費了down事件,那麼viewGroup自己處理事件
- 否則將事件分離成多個MotionEvent,每個MotionEvent只包含對應view感興趣的觸控點的資訊,並派發給對應的子view
viewGroup中的原始碼很多,但大體的邏輯也就這三大部分。理解好MotionEvent和TouchTarget的設計,那麼理解viewGroup的事件分發原始碼也是手到擒來。上面的原始碼我省略了一些細節內容,下面附上完整的viewGroup分發程式碼。
```java
ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一致性檢驗器,用於除錯用途
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 輔助功能,用於輔助有障礙人群使用;
// 如果這個事件是輔助功能事件,那麼他會帶有一個target view,要求事件必須分發給該view
// 如果setTargetAccessibilityFocus(false),表示取消輔助功能事件,按照常規的事件分發進行
// 這裡表示如果當前是目標target view,則取消標誌,直接按照普通分發即可
// 後面還有很多類似的程式碼,都是同樣的道理
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
// 對遮蓋狀態進行過濾
if (onFilterTouchEventForSecurity(ev)) {
// action的高9-16位表示索引值
// 低1-8位表示事件型別
// 只有down或者up事件才有索引值
final int action = ev.getAction();
// 獲取到真正的事件型別
final int actionMasked = action & MotionEvent.ACTION_MASK;
// ACTION_DOWN事件,表示這是一個全新的事件序列,會清除所有的touchTarget,重置所有狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 判斷是否需要攔截
final boolean intercepted;
// down事件或者有target的非down事件則需要判斷是否需要攔截
// 否則直接攔截自己處理
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 此標誌為子view通過requestDisallowInterupt方法設定
// 禁止viewGroup攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 呼叫onInterceptTouchEvent判斷是否需要攔截
intercepted = onInterceptTouchEvent(ev);
// 恢復事件狀態
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 自己消費了down事件
intercepted = true;
}
// 如果已經被攔截、或者已經有了目標view,取消輔助功能的target標誌
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 判斷是否需要取消
// 這裡有很多種情況需要傳送取消事件
// 最常見的是viewGroup攔截了子view的ACTION_MOVE事件,導致事件序列中斷
// 那麼需要傳送cancel事件告知該view,讓該view做一些狀態恢復工作
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 三個變數:
// 是否需要對事件進行分裂,對應多點觸控事件
// newTouchTarget 如果是down或pointer_down事件的新的繫結target
// alreadyDispatchedToNewTouchTarget 是否已經分發給target view了
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 下面部分的程式碼是尋找消費down事件的子控制元件
// 如果沒有取消和攔截進入分發
if (!canceled && !intercepted) {
// 如果是輔助功能事件,我們會尋找他的target view來接收這個事件
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// down或pointer_down事件,表示新的手指按下了,需要尋找接收事件的view
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 多點觸控會有不同的索引,獲取索引號
// 該索引位於MotionEvent中的一個數組,索引值就是陣列下標值
// 只有up或down事件才會攜帶索引值
final int actionIndex = ev.getActionIndex();
// 這個整型變數記錄了TouchTarget中view所對應的觸控點id
// 觸控點id的範圍是0-31,整型變數中哪一個二進位制位為1,則對應繫結該id的觸控點
// 例如 00000000 00000000 00000000 10001000
// 則表示綁定了id為3和id為7的兩個觸控點
// 這裡根據是否需要分離,對觸控點id進行記錄,
// 而如果不需要分離,則預設接收所有觸控點的事件
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清除之前獲取到該觸控id的TouchTarget
removePointersFromTouchTargets(idBitsToAssign);
// 如果子控制元件的數量等於0,那麼不需要進行遍歷只能給viewGroup自己處理
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 使用觸控點索引獲取觸控點位置
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 從前到後建立view列表
final ArrayList preorderedList = buildTouchDispatchChildList();
// 這一句判斷是否是自定義view順序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 遍歷所有子控制元件
for (int i = childrenCount - 1; i >= 0; i--) {
// 獲得真正的索引和子view
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 如果是輔助功能事件,則優先給對應的target先處理
// 如果該view不處理,再交給其他的view處理
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 檢查該子view是否可以接受觸控事件和是否在點選的範圍內
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 檢查該子view是否在touchTarget連結串列中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 連結串列中已經存在該子view,說明這是一個多點觸控事件
// 將新的觸控點id繫結到該TouchTarget上
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 設定取消標誌
// 下一次再次呼叫這個方法就會返回true
resetCancelNextUpFlag(child);
// 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
// 如果消費了,需要生成新的TouchTarget
// 如果沒有消費,說明子view不接受該down事件,繼續迴圈尋找合適的子控制元件
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();
// 儲存該view到target連結串列
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 標記已經分發給子view,退出迴圈
alreadyDispatchedToNewTouchTarget = true;
break;
}
// 輔助功能事件對應的targetView沒有消費該事件,則繼續分發給普通view
ev.setTargetAccessibilityFocus(false);
}// 這裡對應for (int i = childrenCount - 1; i > = 0; i--)
if (preorderedList != null) preorderedList.clear();
}// 這裡對應判斷:(newTouchTarget == null && childrenCount != 0)
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 沒有子view接收down事件,直接選擇連結串列尾的view作為target
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}// 這裡對應if (actionMasked == MotionEvent.ACTION_DOWN...)
}// 這裡對應if (!canceled && !intercepted)
if (mFirstTouchTarget == null) {
// 經過了前面的處理,到這裡touchTarget依舊為null,說明沒有找到處理down事件的子控制元件
// 或者down事件被viewGroup本身消費了,所以該事件由viewGroup自己處理
// 這裡呼叫了dispatchTransformedTouchEvent方法來分發事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 已經有子view消費了down事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
// 遍歷所有的TouchTarget並把事件分發下去
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 表示事件在前面已經處理了,不需要重複處理
handled = true;
} else {
// 正常分發事件或者分發取消事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 這裡呼叫了dispatchTransformedTouchEvent方法來分發事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果傳送了取消事件,則移除該target
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 如果接收到取消獲取up事件,說明事件序列結束
// 直接刪除所有的TouchTarget
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 清除記錄的資訊
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
// 如果僅僅只是一個PONITER_UP
// 清除對應觸控點的觸控資訊
removePointersFromTouchTargets(idBitsToRemove);
}
}// 這裡對應if (onFilterTouchEventForSecurity(ev))
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
```
## View對於事件的分發
不管是viewGroup自己處理事件,還是view處理事件,如果沒有被子類攔截(子類重寫方法),最終都會呼叫到 `view.dispatchTouchEvent` 方法來處理事件。view處理事件的邏輯就比viewGroup簡單多了,因為它不需要向下去分發事件,只需要自己處理。整體的邏輯如下:
1. 首先判斷是否被其他非全屏view覆蓋。這和上面viewGroup的安全性檢查是一樣的
2. 經過檢查之後先檢查是否有onTouchListener監聽器,如果有則呼叫它
3. 如果第2步沒有消費事件,那麼會呼叫onTouchEvent方法來處理事件
- 這個方法是view處理事件的核心,裡面包含了點選、雙擊、長按等邏輯的處理需要重點關注。
我們先看到 `view.dispatchTouchEvent` 方法原始碼:
```java
View.java api29
public boolean dispatchTouchEvent(MotionEvent event) {
// 首先處理輔助功能事件
if (event.isTargetAccessibilityFocus()) {
// 本控制元件沒有獲取到焦點,不處理事件
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// 獲取到焦點,按照常規處理事件
event.setTargetAccessibilityFocus(false);
}
// 表示是否消費事件
boolean result = false;
// 一致性檢驗器,檢驗事件是否一致
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
// 如果是down事件,停止巢狀滑動
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
// 安全過濾,本視窗位於非全屏視窗之下時,可能會阻止控制元件處理觸控事件
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
// 如果事件為滑鼠拖動滾動條
result = true;
}
// 先呼叫onTouchListener監聽器
// 當我們設定onTouchEventListener之後,L
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 若onTouchListener沒有消費事件,呼叫onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 一致性檢驗
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 如果是事件序列終止事件或者沒有消費down事件,終止巢狀滑動
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
```
原始碼內容不長,主要的邏輯內容上面已經講了,其他的都是一些細節的處理。onTouchListener一般情況下我們是不會使用,那麼接下來我們直接看到onTouchEvent方法。
onTouchEvent總體上就做一件事:**根據按下情況選擇觸發onClickListener或者onLongClickListener** ,也就是判斷是單擊還是長按事件,其他的原始碼都是實現細節。onTouchEvent方法正確處理每一個事件型別,來確保點選與長按監聽器可以被準確地執行。理解onTouchEvent的原始碼之前,有幾個重要的點需要先了解一下。
我們的操作模式有按鍵模式、觸控模式。按鍵模式對應的是外接鍵盤或者以前的老式鍵盤機,在按鍵模式下我們要點選一個按鈕通常都是先使用方向游標選中一個button(也就是讓該button獲取到focus),然後再點選確認按下一個button。但是在觸控模式下,button卻不需要獲取焦點。**如果一個view在觸控模式下可以獲取焦點,那麼他將無法響應點選事件,也就是無法呼叫onClickListener監聽器** ,例如EditText。
view辨別單擊和長按的方法是**設定延時任務**,在原始碼中會看到很多的類似的程式碼,這裡延時任務使用handler來實現。當一個down事件來臨時,會新增一個延時任務到訊息佇列中。如果時間到還沒有接收到up事件,說明這是個長按事件,那麼就會呼叫onLongClickListener監聽器,而如果在延時時間內收到了up事件,那麼說明這是個單擊事件,取消這個延時的任務,並呼叫onClickListener。判斷是否是一個長按事件,呼叫的是 `checkForLongClick` 方法來設定延時任務:
```java
// 接收四個引數:
// delay:延時的時長;x、y: 觸控點的位置;classification:長按型別分類
private void checkForLongClick(long delay, float x, float y, int classification) {
// 只有是可以長按或者長按會顯示工具提示的view才會建立延時任務
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
// 標誌還沒觸發長按
// 如果延遲時間到,觸發長按監聽,這個變數 就會被設定為true
// 那麼當up事件到來時,就不會觸控單擊監聽,也就是onClickListener
mHasPerformedLongPress = false;
// 建立CheckForLongPress
// 這是一個實現Runnable介面的類,run方法中回調了onLongClickListener
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
// 設定引數
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
mPendingCheckForLongPress.setClassification(classification);
// 使用handler傳送延時任務
postDelayed(mPendingCheckForLongPress, delay);
}
}
```
上面這個方法的邏輯還是比較簡單的,下面看看 `CheckForLongPress` 這個類:
```java
private final class CheckForLongPress implements Runnable {
...
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
// 在延時時間到之後,就會執行這個任務
// 呼叫onLongClickListener監聽器
// 並設定mHasPerformedLongPress為true
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
...
}
```
延遲時間結束後,就會執行 `CheckForLongPress` 物件,回撥onLongClickListener,這樣就表示這是一個長按的事件了。
另外,在預設的情況下,當我們按住一個view,然後手指滑動到該view所在的範圍之外,那麼系統會認為你對這個view已經不感興趣,所以無法觸發單擊和長按事件。當然,很多時候並不是如此,這就需要具體的view來重寫onTouchEvent邏輯了,但是view的預設實現是這樣的邏輯。
好了,那麼接下來就來看一下完整的 `view.onTouchEvent` 程式碼:
```java
View.java api29
public boolean onTouchEvent(MotionEvent event) {
// 獲取觸控點座標
// 這裡我們發現他是沒有傳入觸控點索引的
// 所以預設情況下view是隻處理索引為0的觸控點
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 判斷是否是可點選的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 一個被禁用的view如果被設定為clickable,那麼他仍舊是可以消費事件的
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
// 如果是按下狀態,取消按下狀態
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 返回是否可以消費事件
return clickable;
}
// 如果設定了觸控事件代理你,那麼直接呼叫代理來處理事件
// 如果代理消費了事件則返回true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果該控制元件是可點選的,或者長按會出現工具提示
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 如果是長按顯示工具類標誌,回撥該方法
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
// 如果是不可點選的view,同時會清除所有的標誌,恢復狀態
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
// 判斷是否是按下狀態
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 如果可以獲取焦點但是沒有獲得焦點,請求獲取焦點
// 正常的觸控模式下是不需要獲取焦點,例如我們的button
// 但是如果在按鍵模式下,需要先移動游標選中按鈕,也就是獲取focus
// 再點選確認觸控按鈕事件
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// 確保使用者看到按下狀態
setPressed(true, x, y);
}
// 兩個引數分別是:長按事件是否已經響應、是否忽略本次up事件
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 這是一個單擊事件,還沒到達長按的時間,移除長按標誌
removeLongPressCallback();
// 只有不能獲取焦點的控制元件才能觸控click監聽
if (!focusTaken) {
// 這裡使用傳送到訊息佇列的方式而不是立即執行onClickListener
// 原因在於可以在點選前觸發一些其他視覺效果
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// 取消按下狀態
// 這裡也是個post任務
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// 如果傳送到佇列失敗,則直接取消
mUnsetPressedState.run();
}
// 移除單擊標誌
removeTapCallback();
}
// 忽略下次up事件標誌設定為false
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
// 輸入裝置源是否是可觸控式螢幕幕
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
// 標誌是否是長按
mHasPerformedLongPress = false;
// 如果是不可點選的view,說明是長按提示工具的view
// 直接檢查是否發生了長按
if (!clickable) {
// 這個方法會發送一個延遲的任務
// 如果延遲時間到還是按下狀態,那麼就會回撥onLongClickListener介面
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
// 判斷是否是滑鼠右鍵或者手寫筆的第一個按鈕
// 特殊處理直接返回
if (performButtonActionOnTouchDown(event)) {
break;