關于事件分發
無非就是當使用者觸摸螢幕或者按鍵操作,首次觸發的是硬體驅動,驅動收到事件後,将該相應事件寫入到輸入裝置節點, 這便産生了最原生态的核心事件。接着,輸入系統取出原生态的事件,經過層層封裝後成為KeyEvent或者MotionEvent ;最後,傳遞給相應的目标視窗(Window)來消費該輸入事件。
一組事件:從手指觸摸螢幕開始,到手指離開螢幕結束。
從程序層面來看事件分發
早在16年,Gityuan大神就有寫過關于Input系統的一些底層源碼文章,詳細的介紹了Android系統輸入事件處理機制。
很明顯,輸入事件的産生和最終的消費,是分别存在于系統程序system_server和具體消費事件的App程序:
InputManagerService
系統程序SysterServer啟動的時候,會開啟核心服務以及其他各種服務,其中就包含InputManagerService,用來處理各種輸入事件,比如觸摸事件和按鍵事件,以及外設鍵入事件等。
可見,我們所關心觸摸事件,隻是衆多輸入事件的一部分。
InputChannle
InputChannle是指定用于在另一個程序中将輸入事件發送到視窗的檔案描述符。
InputChannle會建立socket pair,用于兩個程序的線程間互相通信。
在Activity啟動流程中,ActivityThread的handleResumeActivity會建立ViewRootImpl,并調用其setView方法。
ViewRootImpl.setView會建立InputChannle,并通過WindowInputEvent在native層初始化channle,同時注冊監聽事件。
ViewRootImpl中的InputChannle是socket的用戶端,而系統服務IMS中擷取到的,則是socket的服務端。
從應用内視圖來看事件分發
對于應用内視圖來說,事件分發的起點在Activity,并且經過一個遞歸循環之後,又在Activity中将最終事件處理結果傳回。
由于這裡已經屬于老生常談的點,也就不再進行大篇幅的代碼分析,詳細内容可移步Carson_Ho大神的Android事件分發機制詳解:史上最全面、最易懂,這裡僅引用幾張圖檔,對Activity、ViewGroup和View各個層級,以及整體的工作流程進行一個總結:
Activity層面
ViewGroup層面
View層面
整體工作流程
從具體事件ACTION來看事件分發
在進行觸摸事件分發時,針對不同的ACTION,會有不同的邏輯路線,大概可以拆分為三大類型:ACTION_DOWN、ACTION_MOVE&ACTION_UP以及ACTION_CANCLE。這三種類型的事件,會有不同的邏輯走線:
ViewGroup中分發邏輯走線
View中分發邏輯走線
事件分發關鍵點
ACTION_DOWN的時候不舉手,這組事件就沒機會了
從上圖中可以看出,ViewGroup在分發目前這組事件時,隻有在ACTION_DOWN的時候,才會去尋找TouchTarget,也就是想要處理這組事件的子View。是以如果子View不在ACTION_DOWN的時候“舉手”,那麼這組事件它就沒有機會處理了。
ViewGroup尋找TouchTarget
在一組事件開始分發時,ViewGroup會首先尋找TouchTarget,如果找到了, 那麼這組事件就交個這個TouchTarget處理。ViewGroup在尋找TouchTarget時,通過如下幾個環節來篩選最終的子View:
這裡需要注意兩點:
後添加的子View會先收到事件,也就是越上層的View越先收到
變更子View的Z軸偏移,會影響接收事件的先後順序,Z值越大越先接收
關于FLAG_DISALLOW_INTERCEPT
每當ACTION_DOWN觸發時,會reset此flag,保證ViewGroup的onInterceptTouchEvent可以在每一組事件中都有機會被調用到
子View在處理目前這組事件時,可以适時的關閉父View對目前這組事件後續事件的攔截:
//禁止攔截
getParent().requestDisallowInterceptTouchEvent(true)
//打開攔截
getParent().requestDisallowInterceptTouchEvent(false)
複制代碼
ViewGroup中的super.dispatchTouchEvent()
通過之前的圖解可以看出,當沒有找到touchTarget或者ViewGroup決定攔截時,ViewGroup會從View的角度,去考慮要不要自己消費這個事件。
View.canReceivePointerEvents
在ViewGroup周遊尋找TouchTarget過程中,這個方法也起到了決定性的作用,具體來看一下這個方法:
protected boolean canReceivePointerEvents() {
//可見或目前執行動畫Animation不為null
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
複制代碼
是以在動畫執行完畢時,應該适時的清除view的動畫view.clearAnimation()。
多點觸控的處理
在某些場景下,使用者可以通過多個手指進行互動操作,這就需要在這個場景下,合理的處理使用者的多點觸控行為。
TouchTarget
在進行多點觸控分析前,首先來看一下TouchTarget:
private static final class TouchTarget {
//省略部分代碼
// 儲存的子View引用,
@UnsupportedAppUsage
public View child;
// 目前子View追蹤手指的掩碼
public int pointerIdBits;
// 指向下一個TouchTarget
public TouchTarget next;
//省略部分代碼
複制代碼
在TouchTarget中儲存着消費事件的子View,以及這個子View目前追蹤的手指ids(可為多個手指),還有一個next指向下一個TouchTarget,那麼下一個TouchTarget又是什麼意思呢?
我們知道,頂層ViewGroup會在手指按下的時候,reset一些屬性同時還會尋找TouchTarget。reset屬性時,需要事件隻能是ACTION_DOWN,但是尋找TouchTarget卻還允許觸摸事件為ACTION_POINTER_DOWN。
ACTION_POINTER_DOWN,表示當手指按下時,已經存在其他手指在事件序列中,即還沒有擡起。那麼就可能存在這麼一種情況,第一個手指按在了一個Button上,但是第二根手指按在了另一個Button上,那麼當第二根手指按下時,就會尋找到第二個TouchTarget,這個時候就會将這個新的TouchTarget.next指向mFirstTouchTarget(即第一個),然後将mFirstTouchTarget指向這個新的TouchTarget。
如果你現在是使用的掘金APP,那麼可以試一下長按右上角會彈出分享,然後别松開,再長按文章内容區域,會再彈出menu。
MotionEvent.getActionMask
MotionEvent就是事件分發時的主角,隻有了解了它才能正确的處理觸摸事件。
請先檢視GcsSloop大佬的MotionEvent詳解。
擷取事件類型
1.getAction:單點觸控觸摸事件
2.getActionMask:多點觸控必須使用此方法擷取Action
追蹤事件時,應該瞅準PointerId
int index = event.getActionIndex()
int pointerId = event.getPointerId(index)
複制代碼
多指按下和擡起時,每個手指對應的pointerId是固定不變的,而對應的index會發生變化,變化的規則是保證index是連續的。
多指按下時:
多指擡起時:
多點觸控處理方案
接力型
同一時刻隻追蹤一個手指(如始終追蹤最新按下的手指)的運動軌迹。
協作型
同時追蹤所有觸摸手指運動軌迹,判斷使用者行為(捏撐縮放、多指平移等)。
各自為棧
同時追蹤所有觸摸手指運動軌迹,但是互不影響(如每個手指都能畫畫)。
ViewGroup.setMotionEventSplittingEnabled(false)
此方法可以關閉被調用ViewGroup對多點觸控的支援,再來看一下ViewGroup的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
//省略部分代碼
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//省略部分代碼
if (actionMasked == MotionEvent.ACTION_DOWN
//當ACTION_POINTER_DOWN發生時,必須split為true,才會發起對TouchTarget的尋找
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//省略部分代碼
findTouchTarget
}
}
複制代碼
GestureDetector更便捷的處理使用者觸摸事件
使用手勢監聽器,可以更加便捷的處理使用者手勢,詳細的内容可以移步Carson_Ho大佬博文Android GestureDetector詳解。