該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡量按照先易后難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑒了其他的優質博客,在此向各位大神表示感謝,膜拜!!!另外,本系列文章知識可能需要有一定Android開發基礎和項目經驗的同學才能更好理解,也就是說該系列文章面向的是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中又做了什么呢。。下面我們來。。。。。
1 首先從手指觸摸到屏幕開始
我們知道Android是基於Linux系統的。當輸入設備可用時(這里的輸入設備包括很多設備,比如觸摸屏和鍵盤是Android最普遍也是最標准的輸入設備,另外它還包括外接的游戲手柄、鼠標等),Linux內核會為輸入設置創建對應的設備節點。當輸入設備不可用時,就把對應的設備節點刪除,這也是如果我們的屏幕意外摔碎了或者其他原因導致觸摸屏幕不可用時觸摸沒有反應的根本原因。當我們的輸入設備可用時(我們這里只來講解觸摸屏),我們對觸摸屏進行操作時,Linux就會收到相應的硬件中斷,然后將中斷加工成原始的輸入事件並寫入相應的設備節點中。而我們的Android 輸入系統所做的事情概括起來說就是監控這些設備節點,當某個設備節點有數據可讀時,將數據讀出並進行一系列的翻譯加工,然后在所有的窗口中找到合適的事件接收者,並派發給它。
2 手指進行一系列操作(這里指的是手指的移動,這一步可能沒有)
3 手指抬起或者因其他其他原因(突然間來了個電話之類的)導致事件結束
注:上述第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的分發。
下面我們來詳細分析,請注意,前方高能,請自備紙巾(草稿紙)
上面我們說到了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與窗口。上文中我們也說到了IMS會在所有的窗口中找到合適的事件接收者。IMS是運行在SystemServer進程中,而我們的窗口呢,是在我們的應用進程中。這就引出了我們在Android開發之漫漫長途 Ⅴ——Activity的顯示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw中留下的懸念
`// ② 初始化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源碼並沒有發現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;
}
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源碼把。
@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.
|| (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事件體系
此致,敬禮