很高興碰見你~css
在上一篇文章 Android事件分發機制一:事件是如何到達activity的? 中,咱們討論了觸摸信息從屏幕產生到發送給具體 的view處理的總體流程,這裏先來簡單回顧一下:java
前面的分發步驟咱們清楚了,那麼viewGroup是如何對觸摸事件進行分發的呢?View又是如何處理觸摸信息的呢?正是本文要討論的內容。android
事件處理中涉及到的關鍵方法就是 dispatchTouchEvent
,無論是viewGroup仍是view。在viewGroup中,dispatchTouchEvent
方法主要是把事件分發給子view,而在view中,dispatchTouchEvent
主要是處理消費事件。而主要的消費事件內容是在 onTouchEvent
方法中。下面討論的是viewGroup與view的默認實現,而在自定義view中,一般會重寫 dispatchTouchEvent
和 onTouchEvent
方法,例如DecorView等。git
秉着邏輯先行源碼後到的原則,本文雖然涉及到大量的源碼,但會優先講清楚流程,有時間的讀者仍然建議閱讀完整源碼。面試
事件分發中涉及到一個很重要的點:多點觸控,這是在不少的文章中沒有體現出來的。而要理解viewGroup如何處理多點觸控,首先須要對觸摸事件信息類:MotionEvent,有必定的認識。MotionEvent中承載了觸摸事件的不少信息,理解它更有利於咱們理解viewGroup的分發邏輯。因此,首先須要先理解MotionEvent。api
觸摸事件的基本類型有三種:數組
一個完整的觸摸事件系列是:從ACTION_DOWN開始,到ACTION_UP結束 。這其實很好理解,就是手指按下開始,手指擡起結束。安全
手指可能會在屏幕上滑動,那麼中間會有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP。併發
這是正常的狀況,而若是出現了一些異常的狀況,事件序列被中斷,那麼會產生一個取消事件:ide
因此,完整的事件序列是:從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束 。固然,這是咱們一個手指的狀況,那麼在多指操做的狀況是怎麼樣的呢?這裏須要引入另外的事件類型:
區別於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中所攜帶的信息。
假如如今屏幕上有兩個手指按下,以下圖:
觸摸點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對象內部,維護有一個數組。這個數組中的每一項對應不一樣的觸摸點的信息,以下圖:
數組下標稱爲觸控點的索引,每一個節點,擁有一個觸控點的完整信息。這裏要注意的是,一個觸控點的索引並非一成不變的,而是會隨着觸控點的數目變化而變化。例如當同時按下兩個手指時,數組狀況以下圖:
而當手指a擡起後,數組的狀況變爲下圖:
能夠看到觸控點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。以下圖:
那麼,可不能夠不進行分離?固然能夠。這樣的話每次都把全部觸控點的信息發送給子view。這能夠經過FLAG_SPLIT_MOTION_EVENTS這個標誌進行設置是否要進行分離。
小結一下:
一個觸控點的序列通常狀況下只給一個view處理,當一個view消費了一個觸控點的down事件後,該觸控點的事件序列後續事件都會交給他處理。
事件分離是把一個motionEvent中的觸控點信息進行分離,只向子view發送其感興趣的觸控點信息。
咱們能夠經過設置FLAG_SPLIT_MOTION_EVENTS標誌讓viewGroup是否對事件進行分離
到這裏關於MotionEvent的內容就講得差很少,固然在分離的時候,還須要進行必定的調整,例如座標軸的更改、事件類型的更改等等,放在後面講,接下來看看ViewGroup是如何分發事件的。
這一步能夠說是事件分發中的重頭戲了。不過在理解了上面的MotionEvent以後,對於ViewGroup的分發細節也就容易理解了。
總體來講,ViewGroup分發事件分爲三個大部分,後面的內容也會圍繞着三大部分展開:
大致的流程是:每個事件viewGroup會先判斷是否要攔截,若是是down事件(這裏的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),還須要挨個遍歷子view看看是否有子view消費了down事件,最後再把事件派發下去。
在開始解析以前,必須先了解一個關鍵對象: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遮擋住的時候,那麼有可能被惡意軟件操做發生危險。例如咱們看到的界面是這樣的:
但實際上,咱們看到的這個按鈕時不可點擊的,實際上觸摸事件會被分發到這個按鈕後面的真正接收事件的按鈕:
而後咱們就白給了。這個安全攔截行爲由兩個標誌控制:
具體的源碼以下:
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()
方法。
onInterceptTouchEvent()
方法判斷是否須要攔截。onInterceptTouchEvent方法默認只對一種特殊狀況做了攔截。通常狀況下咱們會重寫這個方法來攔截事件:// 只對一種特殊狀況作了攔截 // 鼠標左鍵點擊了滑動塊 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
方法邏輯中對於事件攔截部分的源碼分析以下:
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事件,無論是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事件的。
接下來咱們看看源碼分析(代碼有點長,須要慢慢分析理解):
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<View> 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不會同時消費事件。
上圖展現了須要事件分離的狀況下進行的事件分發。
在把原MotionEvent拆分紅多個MotionEvent時,不只須要把不一樣的觸控點信息進行分離,還須要對座標進行轉換和改變事件類型:
viewGroup中真正執行事件派發的關鍵方法是 dispatchTransformedTouchEvent
,該方法會完成關鍵的事件分發邏輯。源碼分析以下:
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
中派發事件的代碼部分:
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的事件分發源碼就解析完成了,這裏再來小結一下:
viewGroup中的源碼不少,但大致的邏輯也就這三大部分。理解好MotionEvent和TouchTarget的設計,那麼理解viewGroup的事件分發源碼也是手到擒來。上面的源碼我省略了一些細節內容,下面附上完整的viewGroup分發代碼。
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<View> 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; }
無論是viewGroup本身處理事件,仍是view處理事件,若是沒有被子類攔截(子類重寫方法),最終都會調用到 view.dispatchTouchEvent
方法來處理事件。view處理事件的邏輯就比viewGroup簡單多了,由於它不須要向下去分發事件,只須要本身處理。總體的邏輯以下:
咱們先看到 view.dispatchTouchEvent
方法源碼:
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
方法來設置延時任務:
// 接收四個參數: // 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
這個類:
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
代碼:
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; } // 向上遍歷view查看是否在一個可滑動的容器中 boolean isInScrollingContainer = isInScrollingContainer(); // 若是在一個可滑動的容器中,那麼須要延遲一小會再響應反饋 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 利用消息隊列來延遲檢測一個單擊事件,延遲時間是ViewConfiguration.getTapTimeout() // 這個時間是100ms postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 沒有在可滑動的容器中,直接響應觸摸反饋 // 設置按下狀態爲true setPressed(true, x, y); checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } break; case MotionEvent.ACTION_CANCEL: // 取消事件,恢復全部的狀態 if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: // 通知view和drawable熱點改變 // 暫時不知道什麼意思 if (clickable) { drawableHotspotChanged(x, y); } final int motionClassification = event.getClassification(); final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; int touchSlop = mTouchSlop; // view已經被設置了長按標誌且目前的事件標誌是模糊標誌 // 系統並不知道用戶的意圖,因此即便滑出了view的範圍,並不會取消長按標誌 // 而是延長越界的偏差範圍和檢查長按的時間 // 由於這個時候系統並不知道你是想要長按仍是要滑動,結果就是兩種行爲都沒有響應 // 由你接下來的行爲決定 if (ambiguousGesture && hasPendingLongPressCallback()) { final float ambiguousMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier(); // 判斷此時觸控點的位置是否還在view的範圍內 // touchSlop是一個小範圍的偏差,超出view位置slop距離依舊斷定爲在view範圍內 if (!pointInView(x, y, touchSlop)) { // 移除原來的長按標誌 removeLongPressCallback(); // 延長等待時間,這裏是原來長按等待的兩倍 long delay = (long) (ViewConfiguration.getLongPressTimeout() * ambiguousMultiplier); // 減去已經等待的時間 delay -= event.getEventTime() - event.getDownTime(); // 添加新的長按標誌 checkForLongClick( delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } touchSlop *= ambiguousMultiplier; } // 判斷此時觸控點的位置是否還在view的範圍內 // touchSlop是一個小範圍的偏差,超出view位置slop距離依舊斷定爲在view範圍內 if (!pointInView(x, y, touchSlop)) { // 若是已經超出範圍,直接移除點擊標誌和長按標誌,點擊和長按事件均沒法響應 removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // 取消按下標誌 setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; // 表示用戶在屏幕上用力按壓,加快長按響應速度 if (deepPress && hasPendingLongPressCallback()) { // 移除原來的長按標誌,直接響應長按事件 removeLongPressCallback(); checkForLongClick( 0 /* 延遲時間爲0 */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); } break; } return true; } // 對應if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) return false; }
若是你能看到這裏,說明你對於viewGroup和view的事件處理源碼已經瞭如指掌了。(高興之餘不如給筆者點個贊?(: ~)
最後這裏再來總結一下:
dispatchTouchEvent
方法來分發事件,那麼這裏就會分爲兩種狀況:
dispatchTouchEvent
主要內容是處理事件。首先會調用onTouchListener,若是其沒有處理則會調用onTouchEvent方法。
到此本文的內容就結束了,事件分發的總體流程回顧、學了事件分發有什麼做用、高頻面試題相關文章,將會在後續繼續創做。
在學習過程當中,如下相關資料給了我很是大的幫助,都是很是優秀的文章:
全文到此,原創不易,以爲有幫助能夠點贊收藏評論轉發。
筆者才疏學淺,有任何想法歡迎評論區交流指正。
如需轉載請評論區或私信交流。另外歡迎光臨筆者的我的博客:傳送門