該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,如果能給各位看官帶來一絲啟發或者幫助,那真是極好的。
前言
在上一篇文章中我們上了一個小例子來自定義View,文章比較簡單,閱讀量幾乎沒有,有灌水的嫌疑,(實際上沒有,每一篇文章我都是用心在寫)。這一篇文章呢,我們來看一下Android事件的分發機制。關於這方面的知識大概已經被講爛了。我本人也看了好多關於這方面優質的文章和部落格。可以說是受益匪淺,但是可是總覺得沒有掌握完全。所以我去看了關於底層原始碼的一些知識。然後在這裡分享給大家。
當我們的手指從觸控到螢幕上的各種View開始到這個點選事件結束到底經歷了什麼,我們來詳細分析一下。(Android的輸入系統處理了很多事件,包括按鍵,觸控,以及外接裝置,但是我們這篇文章只分析我們最熟悉也是最常用的觸控事件,這裡的描述也許不太精確,但是卻最為直觀)
我們先上一個總體流程圖
注:上圖中綠色線條表示預設的事件處理流程,即我們沒有做任何處理,事件會按照綠色線條所示的方向由Activity->...ViewGroup..->View->...ViewGroup..->Activity這個U型圖進行傳遞。即一直預設呼叫super.XXX方法。
上圖中黑色線條表示預設Activity->...ViewGroup..->View->...ViewGroup..->Activity這個U型圖的任一節點中(不包括onInterceptTouchEvent)返回了true,事件即結束,不再向下一節點傳遞。
上圖中紅色線條表示一些特殊情況,尤其是ViewGroup,ViewGroup.onInterceptTouchEvent表示詢問當前ViewGroup是否需要攔截此事件即要不要處理,為什麼要“多此一舉”呢,因為ViewGroup.dispatchTouchEvent這個函式的特殊,從上圖可知,該函式返回true,是消費事件,返回false是交由上一級的ViewGroup或者Activity的onTouchEvent。那麼它怎麼向下傳遞事件或者想把事件交給自己的onTouchEvent處理呢,所以ViewGroup多了個onInterceptTouchEvent(View是沒有該函式的),onInterceptTouchEvent起到作用的是分流。onInterceptTouchEvent返回false或者返回super.xxx是向下級View或者ViewGroup傳遞,返回true呢是把事件交給自己的onTouchEvent處理。
我們知道了上圖,,但是Activty的事件又是從哪得到的呢,事件最終返回到Activity的onTouchEvent中又做了什麼呢。。下面我們來。。。。。
- 首先從手指觸控到螢幕開始
我們知道Android是基於Linux系統的。當輸入裝置可用時(這裡的輸入裝置包括很多裝置,比如觸控式螢幕和鍵盤是Android最普遍也是最標準的輸入裝置,另外它還包括外接的遊戲手柄、滑鼠等),Linux核心會為輸入設定建立對應的裝置節點。當輸入裝置不可用時,就把對應的裝置節點刪除,這也是如果我們的螢幕意外摔碎了或者其他原因導致觸控螢幕不可用時觸控沒有反應的根本原因。當我們的輸入裝置可用時(我們這裡只來講解觸控式螢幕),我們對觸控式螢幕進行操作時,Linux就會收到相應的硬體中斷,然後將中斷加工成原始的輸入事件並寫入相應的裝置節點中。而我們的Android 輸入系統所做的事情概括起來說就是**監控這些裝置節點,當某個裝置節點有資料可讀時,將資料讀出並進行一系列的翻譯加工,然後在所有的視窗中找到合適的事件接收者,並派發給它。
- 手指進行一系列操作(這裡指的是手指的移動,這一步可能沒有)
- 手指抬起或者因其他其他原因(突然間來了個電話之類的)導致事件結束
注:上述第2第3步與第1步裡的處理基本相同,但是需要注意的是Android是序列處理事件的,也就是說按下的動作(ACTION_DOWN|ACTION_POINTER_DOWN)處理完成之前是不會處理後續的ACTION_MOVE|ACTION_POINTER_MOVE和ACTION_UP|ACTION_POINTER_UP事件的。並且後續的ACTION_MOVE|ACTION_POINTER_MOVE和ACTION_UP|ACTION_POINTER_UP事件會根據對ACTION_DOWN|ACTION_POINTER_DOWN事件的不同而稍有不同。下面我們先來分析按下的事件ACTION_DOWN|ACTION_POINTER_DOWN的分發。
下面我們來詳細分析,請注意,前方高能,請自備紙巾(草稿紙)
INotify和Epoll機制
上面我們說到了Android 輸入系統所做的事情概括起來說就是監控裝置節點,當某個裝置節點有資料可讀時,將資料讀出並進行一系列的翻譯加工,然後在所有的視窗中找到合適的事件接收者,並派發給它。那麼它是如何做的呢,,我們來具體分析一下。Android 的輸入系統InputManagerService(以下簡稱為IMS)作為系統服務,它像其他系統服務一樣在SystemServer程式中建立。
Linux會為所有可用的輸入裝置在/dev/input目錄在建立event0~n或者其他名稱的裝置節點,Android輸入系統會監控這些裝置節點,具體是通過INotify和Epoll機制來進行監控。而不是通過一個執行緒進行輪詢查詢。
我們先來看一下INotify和Epoll機制(這裡我們只進行簡單的描述,讀者如果有興趣可以留言,我單開一篇文章)
INotify機制
INotify是Linux核心提供的一種檔案系統變化通知機制。它可以為應用程式監控檔案系統的變化,如檔案的新建,刪除等。
//建立INotify物件,並用描述符inotifyFd 描述它
int inotifyFd = inotify_init();
/*
新增監聽
inotify_add_watch函式引數說明
inotifyFd:上面建立的INotify物件的描述符,當監聽的目錄或檔案發生變化時記錄在INotify物件
“/dev/input”:被監聽的檔案或者目錄
IN_CREATE | IN_DELETE:事件型別
綜合起來下面的程式碼表示的意思就是當“/dev/input”下發生IN_CREATE | IN_DELETE(建立或者刪除)時即把這個事件寫入到INotify物件中
*/
int wd = inotify_add_watch(inotifyFd, "/dev/input", IN_CREATE|IN_DELETE )
Epoll機制
在上述INotify機制中我們知道了我們只需關心inotifyFd這個描述符就行了,可是事件是隨機發生的,我們也不會本末倒置的採用輪詢的方式輪詢這個描述符,因為如果這樣做的話會浪費大量系統資源。這時候我們Linux的另一個機制就派上用場了,即Epoll機制。Epoll機制簡單的說就是使用一次等待來獲取多個描述的可讀或者可寫狀態。這樣我們不必對每一個描述符建立獨立的執行緒進行阻塞讀取,在避免了資源浪費的同時獲得較快的相應速度。
至此原始輸入事件已經讀取完畢,Android輸入系統對原始輸入事件進行翻譯加工以及派發的詳細過程很複雜。我們這裡只分析其中一部分——IMS與視窗。
Avtivity,Window,PhoneWindow,以及ViewRootImpl之間的聯絡
上文中我們也說到了IMS會在所有的視窗中找到合適的事件接收者。IMS是執行在SystemServer程式中,而我們的視窗呢,是在我們的應用程式中。這就引出了我們在上文中留下的懸念
// ② 初始化mInputChanel。InputChannel是視窗接收來自InputDispatcher的輸入事件的管道。這部分內容我們將在下一篇介紹。
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
...
...
// ③ 如果mInputChannel不為空,則建立mInputEventReceiver用於接收輸入事件。
if (mInputChannel != null) {
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
讀者看到這裡該疑惑了,這個不是在ViewRootImpl.setView方法中說的嗎,跟現在講的有關係嗎?且聽我娓娓道來。在上幾篇部落格中我們介紹了Avtivity,Window,PhoneWindow,以及ViewRootImpl這些概念之間 到底有什麼關係呢。
我們從前幾篇中就知道了Activity的啟動流程,Activity物件最先建立,但是Activity的顯示是依靠其內部物件Window mWindow,而Window是個抽象類,所以mWindow指向的實際上是Window的實現類PhoneWindow的物件。PhoneWindow作為顯示的載體,ViewRootImpl的measure、layout以及draw才是View顯示的動力所在。我們執行專案,看到了一個MainActivity,我們點選MainActivity的某個View(如Button了或者其他),實際上我們是點選了螢幕上的某個點。由IMS對這個原始事件進行翻譯加工並找到我們的PhoneWindow,並向PhoneWindow派發事件。整個過程可用如下流程圖表示。
注:上面的流程圖中省略了很多細節,意在讓讀者對Android輸入系統有個更整體的把控。
通過上面的流程圖我們知道,當我們的PhoneWindow建立完成之後,我們也在該Window上註冊了InputChannel並與IMS通訊,IMS把事件寫入InputChannel,WindowInputEventReceiver對事件進行處理並最終還是通過InputChannel反饋給IMS。
下面我們來稍微介紹下InputChannel和WindowInputEventReceiver。
InputChannel
InputChannel的本質是一對SocketPair(非網路套接字)。套接字可以用於網路通訊,也可以用於本機內的程式通訊。程式間通訊的一種方式,具體解釋讀者可自行參看《深入理解Android 卷Ⅲ》》中的5.4.1節。
WindowInputEventReceiver
得到InputChannel後,便用它建立WindowInputEventReceiver,WindowInputEventReceiver繼承於InputEventReceiver,InputEventReceiver物件可以接收來自InputChannel的輸入事件,並觸發其onInputEvent方法的回撥。我們這裡的是WindowInputEventReceiver,所以我們來看一下這個類
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
//重寫了onInputEvent方法,所以當InputChannel有事件時,會觸發
//WindowInputEventReceiver.onInputEvent(),而其內部直接呼叫enqueueInputEvent
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
@Override
public void onBatchedInputEventPending() {
if (mUnbufferedInputDispatch) {
super.onBatchedInputEventPending();
} else {
scheduleConsumeBatchedInput();
}
}
@Override
public void dispose() {
unscheduleConsumeBatchedInput();
super.dispose();
}
}
那我們來看一下enqueueInputEvent
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
...
//① 將InputEvent對應的InputEventReceiver封裝為一個QueuedInputEvent
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
//② 將新建的QueuedInputEvent 追加到mPendingInputEventTail所表示的一個單向連結串列中
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
if (processImmediately) {
//③ 如果第三個引數為true,則直接在當前執行緒中開始對輸入事件的處理工作
doProcessInputEvents();
} else {
//④ 否則將處理事件的請求傳送給主執行緒的Handler,隨後進行處理
scheduleProcessInputEvents();
}
}
我們來看doProcessInputEvents
void doProcessInputEvents() {
//遍歷整個輸入事件佇列,並逐一處理
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
//deliverInputEvent()方法會將完成單個事件的整個處理流程
deliverInputEvent(q);
}
...
}
而deliverInputEvent方法進行一系列呼叫最終會呼叫我們的processPointerEvent()方法
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
// 此時ViewRootImpl會將事件的處理權移交給View樹的根節點,呼叫dispatchPointerEvent函式
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}
在processPointerEvent我們看到ViewRootImpl會將事件的處理權移交給View樹的根節點,呼叫dispatchPointerEvent函式,即mView,而這個mView就是我們熟知的DecorView
在ActivityThread.handleResumeActivity方法中有如下程式碼
//decor即DecorView,l是佈局引數WindowManager.LayoutParams
wm.addView(decor, l);
觸控事件分發詳解
DecorView
我們下面即分析DecorView,我們開啟DecorView原始碼並沒有發現dispatchPointerEvent,彆著急,別上火,,那麼這個dispatchPointerEvent肯定在DecorView父類裡面了,,我們開啟View原始碼,,果然找到了,該函式如下
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//事件如果是Touch事件,毫無疑問我們的是啊
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
這個時候我們要去看View.dispatchTouchEvent嗎??NO!!!!!我們應該看DecorView.dispatchTouchEvent(DecorView重寫了dispatchTouchEvent)
DecorView.dispatchTouchEvent宣告如下
DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/**獲取Window.Callback,Window.Callback是個介面,這裡的mWindow是PhoneWindow,呼叫*
*PhoneWindow.getCallback(),但是PhoneWindow並沒有實現該方法,所以我們找到了
*Window.getCallBack()方法。Window.getCallBack()方法返回Callback型別的mCallback
*/
final Window.Callback cb = mWindow.getCallback();
/**
*如果cb不為空並且window沒有被銷燬 mFeatureId < 0 表示是application的DecorView,
*比如Activity、Dialog把事件傳給cb,否則把事件傳遞給父類的dispatchTouchEvent
*/
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
Window.java
public final Callback getCallback() {
return mCallback;
}
Callback.java
public interface Callback {
...//省略一部分函式
public boolean dispatchTouchEvent(MotionEvent event);
...
}
我們來看這個Window.Callback ,既然有getCallback(),那麼應該有setCallback為mCallback賦值。我們
我們在Activity的attach方法中看到如下程式碼
Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
......
//建立PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
......
//設定當前Activity為Window.Callback,那麼毫無疑問,Activity類或者其父類實現了Window.Callback介面
mWindow.setCallback(this);
......
}
我們來看Activity類的宣告
果然如此。那麼我們就來看看Activity.dispatchTouchEvent
Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//在這裡我們又把事件給了PhoneWindow.superDispatchTouchEvent方法根據其返回值,
//若返回值為true,那麼dispatchTouchEvent返回true,我們Activity的onTouchEvent方法無法得到執行
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//這裡就是我們的Activity的onTouchEvent方法
return onTouchEvent(ev);
}
那我們要看PhoneWindow.superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//兜兜轉轉一大圈,還是把事件交給我們的DecorView,
//DecorView繼承自FrameLayout,FrameLayout呢又繼承自ViewGroup,
//所以作為一個ViewGroup,DecorView繼續向其子View派發事件,其流程我在文章的開頭就已經給了
return mDecor.superDispatchTouchEvent(event);
}
總結:兜兜轉轉一大圈我們神經都被繞彎了,我們在這裡總結一下,當我們觸控(點選)螢幕時,Android輸入系統IMS通過對事件的加工處理再合適的Window接收者並通過InputChannel向Window派發加工後的事件,並觸發InputReceiver的onInputEvent的呼叫,由此產生後面一系列的呼叫,把事件派發給整個控制元件樹的根DecorView。而DecorView又上演了一出偷樑換柱的把戲,先把事件交給Activity處理,在Activity中又把事件交還給了我們的DecorView。自此沿著控制元件樹自上向下依次派發事件。
我們總算把ACTION_DOWN的事件分發分析完畢了,ACTION_DOWN事件可以說是所有觸控事件的起點。我們觸控了螢幕,並引發ACTION_DOWN的事件,然後可能經過一系列的ACTION_MOVE事件,最後是ACTION_UP事件,至ACTION_UP,這整個事件序列算是完成了。我們前面分析了ACTION_DOWN事件,那麼ACTION_MOV和ACTION_UP呢,ACTION_MOV和ACTION_UP的事件分發與ACTION_DOWN並不完全相同。為什麼這麼說呢,是因為他們很相似,但是稍微有些不同。你在執行ACTION_DOWN的時候返回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個事件(如ACTION_DOWN)返回true,才會收到ACTION_MOVE和ACTION_UP的事件。那麼這句話是什麼意思呢?我們來看一下不同情況下事件派發圖。
我們在ViewGroup1中的dispatchTouchEvent中消費事件!
我們在ViewGroupX中的dispatchTouchEvent中消費事件!
我們在View中的dispatchTouchEvent中消費事件!
我們在View中的onTouchEvent中消費事件!
特殊情況1 :我們在ViewGroupX中的onTouchEvent中消費事件!
特殊情況2 :我們在ViewGroupX中的dispatchTouchEvent中返回false並在ViewGroup1中的onTouchEvent中消費事件!
還有種種情況我就不畫圖了。。為什麼會產生上面的結果呢?我們還是來看一下ViewGroup的dispatchTouchEvent原始碼把。
ViewGroup dispatchTouchEvent原始碼分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//表示視窗是否為模糊視窗(FILTER_TOUCHES_WHEN_OBSCURED),
//如果是視窗,則表示不希望處理改事件。(如dialog後的視窗)
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/** 第①步 重新設定狀態 開始*/
// 處理初始的按下動作
if (actionMasked == MotionEvent.ACTION_DOWN) {
//重新設定狀態等,比較重要的是設定mFirstTouchTarget == null,
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/** 第①步 重新設定狀態 結束*/
/** 第②步 檢查是否攔截 開始*/
// 檢查是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//如果是ACTION_DOWN事件或者mFirstTouchTarget != null
//這裡我們去問ViewGroup是否允許攔截,如果允許攔截,我們再去問onInterceptTouchEvent
......
} else {
//如果不是MotionEvent.ACTION_DOWN事件並且mFirstTouchTarget 為空,直接攔截
intercepted = true;
}
/** 第②步 檢查是否攔截 結束*/
......
/** 第③步 向子View派發 開始*/
if (!canceled && !intercepted) {//如果沒有取消並且當前ViewGroup沒有攔截事件
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//判斷事件型別,如果是ACTION_DOWN或者ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE則進入
if (newTouchTarget == null && childrenCount != 0) {
......
......
//獲取子View並迴圈向子View派發事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//如果當前ViewGroup的子View消費了事件,則進入if體
......
//賦值newTouchTarget和mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
......
}
}
/** 第③步 向子View派發 結束*/
/** 第④步 額外的處理 開始*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
/**這個判斷十分重要:
*
*我們在上面的過程中就知道倘若我們沒有攔截即intercepted = false;如果事件是ACTION_DOWN
*或者ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE我們會進入迴圈子View並派發事件的過程,
*如果子View也不想處理該事件即dispatchTransformedTouchEvent()函式返回了false,那麼此時ViewGroup的mFirstTouchTarget == null
*倘若我們重寫了onInterceptTouchEvent並返回true,那麼intercepted = true即進行攔截,
*那麼就不會進入我們的第③步,直接來到第④步,這時當前ViewGroup的mFirstTouchTarget == null
mFirstTouchTarget == null的條件下會呼叫dispatchTransformedTouchEvent
*/
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
/** 第④步 額外的處理 結束*/
......
return handled;
}
我們從上面的程式碼可以更清晰的瞭解到ACTION_DOWN的派發過程,現在還存疑的就是這個mFirstTouchTarget了,我們在觸發ACTION_DOWN的時候,ViewGroup會根據事件掩碼actionMask判斷ACTION_DOWN,並重置一些狀態,重置狀態的過程中就包括把mFirstTouchTarget設為null,我們第一次進入第三步時找到合適的子View並向其派發事件,如果子View消費了ACTION_DOWN事件,則呼叫addTouchTarget進行賦值,我們來看一下這個函式
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//在這裡我們可以看到mFirstTouchTarget 指向了子View
mFirstTouchTarget = target;
return target;
}
有上面的程式碼可知mFirstTouchTarget是ViewGroup的一個成員變數,每一個ViewGroup都持有這個mFirstTouchTarget。
這個mFirstTouchTarget是個單向連結串列,表示的是當前ViewGroup的子View有沒有消費ACTION_DOWN事件,如果消費了ACTION_DOWN事件,就如上面程式碼中第③步的時候描述的一樣給mFirstTouchTarget賦值,如果當前ViewGroup的子View沒有消費ACTION_DOWN事件,即把事件分發給子View的這個dispatchTransformedTouchEvent()函式返回了false,不進入if體,mFirstTouchTarget還是為null。
我們接著來看第④步,結合上圖中的特殊情況1,我們在ViewGroupX中的onTouchEvent中消費了事件。那麼對於ViewGroupX來說,它的mFirstTouchTarget==null,因為它的子View並沒有消費事件,對於ViewGroup1來說它的mFirstTouchTarget != null,因為它的子View ViewGroupX消費了事件,以此類推最後得到的mFirstTouchTarget 連結串列類似於下圖
由於ACTION_MOVE|ACTION_UP事件不符合第③步時進入獲取子View並迴圈派發的條件,當是ACTION_MOVE|ACTION_UP事件會直接來到第④步,判斷當前ViewGroup的mFirstTouchTarget 是否為空,由上圖可知不為空,那麼進入第④步else體,在第④步else體內依據下圖的連結串列逐一向子View派發事件。所以ACTION_MOVE|ACTION_UP事件只派發到ViewGroupX並交由ViewGroupX的onTouchEvent處理,不再向下派發。
那我們再來看一下mFirstTouchTarget == null的條件下呼叫的dispatchTransformedTouchEvent函式
引數分別是ev, canceled, null, TouchTarget.ALL_POINTER_IDS
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) {
//child為空呼叫父類即View的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//child為空呼叫父類即View的dispatchTouchEvent
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;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
//child為空呼叫父類即View的dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
上面的函式我們就不仔細分析了,不過註釋裡寫的很明白,只要流程正常的話,我們都會呼叫父類的dispatchTouchEvent
我們來看一下View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (onFilterTouchEventForSecurity(event)) {
......
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//這裡判斷有沒有為View是否可用Enabled並且檢查是否設定了TouchListener,如果設定了,則觸發TouchListener的onTouch
result = true;
}
if (!result && onTouchEvent(event)) {//如果當前View沒有設定listener資訊,事件也沒有被滾動條消費這裡回撥了我們的onTouchEvent。所以如果為當前View設定了TouchListenerb並在TouchListener的onTouch函式中返回了true,那麼,該View的onTouchEvent將無法得到回撥。
result = true;
}
}
......
return result;
}
本篇總結
本篇文章詳細分析了View的事件體系(寫這一篇文章真是不容易啊)。作為所有觸控事件的起點ACTION_DOWN|ACTION_POINTER_DOWN來說,Android對其的處理很精細,尤其是ViewGroup對其的處理。
-
首先重置狀態,這是因為一個新的事件序列開始了,重置狀態中比較重要的就是這個mFirstTouchTarget了,mFirstTouchTarget作為ViewGroup的成員變數記錄當前ViewGroup下的子View是否消費了該ACTION_DOWN|ACTION_POINTER_DOWN事件。這個子View的意思也不僅僅是直接子View。假如有這樣一個結構
<ViewGroup1>
<ViewGroup2> <ViewGroup3> <ViewGroup4> <View> </View> </ViewGroup4> </ViewGroup3> </ViewGroup2>
</ViewGroup1>
假設是View消費了ACTION_DOWN|ACTION_POINTER_DOWN事件,那麼ViewGroup1的mFirstTouchTarget就是ViewGroup2->ViewGroup3->ViewGroup4->View
- 如果ViewGroup子View消費了事件,那麼記錄mFirstTouchTarget,ACTION_DOWN|ACTION_POINTER_DOWN事件結束,如果沒有子View消費此事件,mFirstTouchTarget為null。後續的ACTION_MOVE|ACTION_UP事件會根據上一步中的mFirstTouchTarget進行分發。若為null,呼叫父類的即View的dispatchTouchEvent,該函式內部會先判斷Listener資訊,並呼叫listener的onTouch方法,根據onTouch的返回值決定是否繼續呼叫當前ViewGroup的onTouchEvent方法;若不為null,則根據mFirstTouchTarget連結串列進行分發後續的ACTION_MOVE|ACTION_UP事件。
希望讀者能多看幾遍上面的分析。相信你一定會有收穫的
下篇預告
在下一篇文章中我們將進行實戰專案,也是對我們前幾篇文章的實際應用。老話說的好,紙上得來終覺淺,絕知此事要躬行。下一篇甚至幾篇我們就來自定義ViewGroup並重點探討滑動衝突如何解決。滑動衝突解決的基礎是今天這篇的View事件體系
參考博文
此致,敬禮