天天看點

android 分發事件,Android必知必會——事件分發機制

關于事件分發

無非就是當使用者觸摸螢幕或者按鍵操作,首次觸發的是硬體驅動,驅動收到事件後,将該相應事件寫入到輸入裝置節點, 這便産生了最原生态的核心事件。接着,輸入系統取出原生态的事件,經過層層封裝後成為KeyEvent或者MotionEvent ;最後,傳遞給相應的目标視窗(Window)來消費該輸入事件。

一組事件:從手指觸摸螢幕開始,到手指離開螢幕結束。

從程序層面來看事件分發

早在16年,Gityuan大神就有寫過關于Input系統的一些底層源碼文章,詳細的介紹了Android系統輸入事件處理機制。

很明顯,輸入事件的産生和最終的消費,是分别存在于系統程序system_server和具體消費事件的App程序:

android 分發事件,Android必知必會——事件分發機制

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層面

android 分發事件,Android必知必會——事件分發機制

ViewGroup層面

android 分發事件,Android必知必會——事件分發機制

View層面

android 分發事件,Android必知必會——事件分發機制

整體工作流程

android 分發事件,Android必知必會——事件分發機制

從具體事件ACTION來看事件分發

在進行觸摸事件分發時,針對不同的ACTION,會有不同的邏輯路線,大概可以拆分為三大類型:ACTION_DOWN、ACTION_MOVE&ACTION_UP以及ACTION_CANCLE。這三種類型的事件,會有不同的邏輯走線:

ViewGroup中分發邏輯走線

android 分發事件,Android必知必會——事件分發機制

View中分發邏輯走線

android 分發事件,Android必知必會——事件分發機制

事件分發關鍵點

ACTION_DOWN的時候不舉手,這組事件就沒機會了

從上圖中可以看出,ViewGroup在分發目前這組事件時,隻有在ACTION_DOWN的時候,才會去尋找TouchTarget,也就是想要處理這組事件的子View。是以如果子View不在ACTION_DOWN的時候“舉手”,那麼這組事件它就沒有機會處理了。

ViewGroup尋找TouchTarget

在一組事件開始分發時,ViewGroup會首先尋找TouchTarget,如果找到了, 那麼這組事件就交個這個TouchTarget處理。ViewGroup在尋找TouchTarget時,通過如下幾個環節來篩選最終的子View:

android 分發事件,Android必知必會——事件分發機制

這裡需要注意兩點:

後添加的子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是連續的。

多指按下時:

android 分發事件,Android必知必會——事件分發機制

多指擡起時:

android 分發事件,Android必知必會——事件分發機制

多點觸控處理方案

接力型

同一時刻隻追蹤一個手指(如始終追蹤最新按下的手指)的運動軌迹。

協作型

同時追蹤所有觸摸手指運動軌迹,判斷使用者行為(捏撐縮放、多指平移等)。

各自為棧

同時追蹤所有觸摸手指運動軌迹,但是互不影響(如每個手指都能畫畫)。

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詳解。