1.觸摸消息是消息獲取模塊直接派發給應用程序的。
2.觸摸消息在處理時, 需要根據觸摸坐標計算該消息應該派發給哪個View/ViewGroup, 在案件取消處理中不存在 該計算過程。
3.沒有類似”系統按鍵”的”系統觸摸鍵”, 應用程序可完全控制觸摸行為。
4.子視圖優先父視圖處理消息, 即首先是子視圖處理該消息,只有當子視圖消耗該消息時, 父視圖才有機會處理 該消息。
觸摸消息總體派發過程
1.進行物理像素到邏輯像素的轉換。
2.如果是DOWN消息, 調用ensureTouchMode(true)函數則進入觸摸模式,與之相反的是”非觸摸模式”,即按鍵模式。
3.將屏幕坐標轉換到視圖坐標。
4.調用mView.dispathTouchEvent()將消息派發給根視圖,該函數內部會繼而將消息派發到整個View樹。
5.如果以上根視圖以及其所有子視圖都沒有消耗該消息,最后處理屏幕邊界偏移。屏幕邊界偏移在程序中用英文edge slop表示,它的作用是當用戶正好觸摸到屏幕邊界時,系統自動對原始消息進行一定的偏移,然后在新的偏移后的位置上尋找是否有匹配的視圖,如果有則將消息派發到該視圖。
根視圖內部消息派發過程
首先來看mView.dispatchTouchEvent()的派發過程。該函數是在ViewRoot中調用的,mView的類型可能有兩種情況,對于應用窗口而言,mView是一個PhoneWindow中的DecorView類型,對于非應用窗口而言,mView是一般的ViewGroup類型。
在DecorView中,首先判斷是否存在Callback對象,它和按鍵消息派發時的Callback對象一樣,就是Activity類。如果沒有Callback對象,則直接調用DecorView基類ViewGroup中的dispatchTouchEvent()函數。
在Activity中,dispatchTouchEvent()的過程如下。
1、如果ACTION_DOWN消息,則調用onUserInteraction()。
2、調用所包含的Window對象的superDispatchTouchEvent()。
3、如果Window類沒有消耗該消息,則調用onTouchEvent()。
下面看看Window類中的superDispatchTouchEvent()。此時Window類的實現就是PhoneWindow類,該函數繼而調用mDecor的superDispatchTouchEvent(),而在DecorView的該函數中又調用super.dispatchTouchEvent,即ViewGroup的dispatchTouchEvent函數。注意這里的調用過程,一般的消耗處理流程是當上一步沒有消耗消息時才執行下一個處理邏輯,而在跟視圖DecorView中,則是當沒有Callback時才調用ViewGroup的消息處理邏輯,而不是當Callback沒有消耗消息時才調用ViewGroup的消息處理邏輯,原因就是Callback本身就會調用ViewGroup的消息處理邏輯。
ViewGroup的內部消息派發過程
ViewGroup內部的處理邏輯也采用遞歸方式,但與按鍵處理的遞歸有所不同。觸摸消息處理中首先會把消息派發給View樹中最后一個子視圖,如果子視圖沒有消耗該消息,才遞歸派發給其父視圖,而在按鍵消息處理時,遞歸的過程正好相反。
1、將布局左邊轉化為視圖坐標。
為什么要轉換呢?因為接下來要判斷該坐標落到了該ViewGroup中的哪個子視圖中,子視圖的位置都是相對于該ViewGroup的視圖坐標的。
2、處理DOWN消息,其作用是判斷該視圖坐標落到了哪個子視圖中。
(1)首先判斷ViewGroup本身是否被禁止獲取Touch消息,如果沒有禁止,并且回調函數onInterceptTouchEvent()中沒有消耗該消息,則意味著該消息可以傳遞給子視圖。如果子視圖消耗了該DOWN消息,則直接返回true。
(2)開始尋找子視圖。調用child.getHitRect(frame)函數獲取該子視圖在父視圖中的布局坐標,即該ViewGroup為該child分配的位置是什么,這個位置相對于該child來講是布局坐標,而相對于ViewGroup來講卻是視圖坐標,參數frame是執行完畢后的位置輸出矩形。得到位置后,就可以調用frame.contain()方法判斷該消息位置是否被包含到了該child中,如果包含,并且該child也是一個ViewGroup,則準備遞歸調用該child的dispatchTouchEvent(),在調用之前,首先需要把坐標重新轉換到child的坐標系中。
(3)完成了遞歸操作錢的坐標轉換工作,接下來判斷該child是否是ViewGroup類。如果是就遞歸調用到ViewGroup的dispatchTouchEvent(),重新從第一步開始執行,如果child不是ViewGroup,而是一個View,則意味著遞歸調用的結束。
3、如果是UP活著CANCEL消息,則消除mGroupFlags中的FLAG_DISALLOW_INTERCEPT標識,即允許該ViewGroup截獲消息。
4、判斷target變量是否為空。空代表了所有的子窗口沒有消耗該消息,所以該ViewGroup本身需要處理該消息。在第二步中,如果匹配到某個child, 并且該child消耗了消息后,會將該child賦值給父視圖中的mMotionTarget變量。在該步中,首先要還原消息的原始位置,因為在第二步中,為了判斷子視圖是否包含該消息中的位置,對位置進行了從布局坐標到視圖坐標的轉換,而此時則需要把視圖坐標重新轉換為布局坐標,因為接下來要調用super.dispatchTouchEvent(),即View類的該函數。View類中處理該函數時,需要布局坐標。轉換之后,直接調用super.dispatchTouchEvent(),并返回其執行結果,該函數內部僅僅是回調onTouchEvent(),調用之前先判斷mPrivateFlags中是否包含CANCEL_NEXT_UP_EVENT標識,該表示一般情況下不會存在,如果存在,則將消息的action類型改為ACTION_CANCEL。
5、處理target存在,并且變量disallowInercept為false;即允許截獲,在默認況下ViewGroup都是允許截獲消息的,只有當該ViewGroup的子視圖調用父視圖的requestDisallowInterceptTouchEvent()函數時,方可禁止父視圖再次截獲消息。但每次UP消息或者CANCEL消息之后,該Viewgroup又會重新截獲信息。注意,在本步中,如果不允許截獲消息,那么也就不會調用onInterceptTouchEvent()函數了,如果允許,并且onTerceptTouchEvent()消耗了該消息,才執行本步的操作。
6、在大多數情況下都會執行到該步,即target存在,并且ViewGroup本身不允許截獲消息或者允許截獲但是卻沒有消耗消息,于是調用target.dispatchTouchEvent()把該消息繼續交給目標視圖處理。在調用該函數前需要檢查target中是否聲明過要取消隨后的消息,即mPrivateFlags中包含CANCEL_NEXT_UP_EVENT,如果是,則把消息action值修改為CANCEL,置空mMotionTarget變量,因為target不想處理接下來的消息,那么就可以認為沒有target了。
以上就是Touch消息在ViewGroup內部的遞歸和派發,在這里注意區分onInterceptTouchEvent()和onTouchEvent()。
onInterceptTouchEvent()是在ViewGroup中定義的,即只有ViewGroup的子類能夠重載該方法。而onTouchEvent()函數有兩個定義,一個是在View類中定義的,所有View類的子類都可以重載該方法,包括ViewGroup,另一個是在Activity中定義的,用戶Activity可以重載該函數。View系統的消息處理機制中,會先執行視圖內部的onTouchEvent,如果沒有處理,才會調用Activity中的onTouchEvent()。
另外,對于ViewGroup而言,在一般情況下會先調用onInterceptTouchEvent(),只有當該函數沒有消耗掉消息,并且其包含的子視圖也沒有消耗掉該消息時,才會執行該ViewGroup的onTouchEvent()。而對于View而言,沒有onInterceptTouchEvent()被調用。但并不是所有的消息處理過程都是先調用onInterceptTouchEvent(), 只有兩種情況才會調用到onInterceptTouchEvent()。
1.即在以上第2步驟中,當時DOWN消息,并且ViewGroup允許截獲消息時。
2.即在以上第5步驟中,當ViewGroup中存在target對象,并且允許截獲消息時。
View內默認消息派發過程
1、調用onFilterTouchEventForSecurity() 處理窗口處于模糊顯示狀態下的消息。所謂的模糊顯示是指,應用程序可以設置當前窗口為模糊狀態,此時窗口內部的所有視圖將顯示為模糊效果。這樣做的目的是為了隱藏窗口中的內容,對于其中各個視圖而言,可以設置該視圖的FILTER_TOUCHES_WHEN_OBSCURED標識,如果存在該標識,則意味著用戶希望不要處理該消息。
2、回調視圖監聽者的onTouch()函數,如果監聽者消耗了該消息,則直接返回。
3、調用onTouchEvent(),應用程序可以重載該函數,但如果沒有重載的話,該函數內部有默認的執行方式,默認的執行流程如下:
(1)判斷該視圖是否為disable狀態,如果是,什么都不做,返回true,即消耗該消息
(2)處理消息代理TouchDelegate。所謂的消息代理是指,可以給某個View指定一個消息處理代理,當View收到消息時,首先將該消息派發給其代理進行處理。如果代理內部消耗了該消息,則View不需要再進行任何處理;如果代理沒有處理,則View繼續按照默認的邏輯進行處理。該類的目的是為了擴大點擊區。(沒有實現)
(3)判斷該視圖是否是可以點擊的,如果不可點擊,則直接返回false,即不處理該消息。否則,真正開始執行觸摸消息的默認處理邏輯,該邏輯中分別處理了ACTION_DOWN、MOVE和UP消息,
1??在ACTION_DOWN消息中,給mPrivateFlags變量添加PRESSED標識,并將變量mHasPerformLongPress置為false,然后啟動tap監測,即發送一個異步延遲消息,延遲時間為ViewConfigration.getTapTimeout()。
2??對于觸摸消息而言,消息本身沒有repeat的屬性,這與按鍵消息不同,一次觸摸消息只有一個DOWN消息,接下來就是連續的MOVE消息,并最終以UP消息結束。
3??處理ACTION_UP消息,代碼中判斷該UP消息是發生在哪一個檢測時間段中,并據此進行不同的處理。
4、處理ACTION_CANCEL消息,這里只需要清除PRESSED標識,并改變視圖狀態,然后再關閉tap檢測即可。
至此Touch消息的一次處理過程就結束了。