Android Touch事件傳遞機制全面解析(從WMS到View樹)
轉眼間近一年沒更新部落格了,工作一忙起來,很難有時間來寫部落格了,由於現在也在從事Android開發相關的工作,因此以後的博文也會更多地專注於這一塊。
這篇文章準備從原始碼層面為大家帶來Touch事件的傳遞機制,我這裡分析的原始碼時Android4.4的。說到分析原始碼,光看肯定是不行的,一定要親自去跟,並且要邊跟邊思考,所以在下一篇中,會有一個Demo來為大家詳細分析原始碼的走向。
下面進入正題,先來看下Android中事件的分類:
1、鍵盤事件:主要是指按下虛擬鍵盤的某個按鍵、或者機身的物理按鍵時產生的事件。
2、滑鼠事件:Android4.0之後增加了對滑鼠事件的監控,如ACTION_HOVER_ENTER。
3、觸控式螢幕事件:凡是觸控螢幕而產生的事件都是觸控式螢幕事件,觸控式螢幕事件包括很多,比如單點觸控、多點觸控)、軌跡球事件等。
我們這裡主要講解單點觸控事件,也就是Touch事件的傳遞,首先看下Touch事件的完整傳遞過程:
1、首先需要明白,Android中,Touch事件的分發分服務端和應用端。在Server端由WindowManagerService(WMS,視窗管理服務,不懂的自行腦補)負責採集和分發的,在client端則是由ViewRootImpl(內部有個mView變數指向View樹的根 ,負責控制View樹的UI繪製和事件訊息的分發)負責分發的。
2、WMS在啟動之後,經過逐層呼叫,會在native層啟動兩個執行緒:InputReaderThread和InputDispatchThread,前者用來讀取輸入事件,
後者用來分發輸入事件,輸入事件經過nativie層的層層傳遞,最終會傳遞到java層的ViewRootImpl中,呼叫
ViewPostImeInputStage(ViewRootImpl的內部類)中的各個方法來分發不同的事件,而Touch事件是在processPointerEvent方法進行分發的(這部分程式碼很單,可自行檢視)。
3、processPointerEvent方法中呼叫mView.dispatchPointerEvent(event)方法,這裡的mView就是在建立視窗後通過呼叫root.setView傳進
來的DecorView,而dispatchPointerEvent方法會對event進行判斷,如果是Touch事件的話,就呼叫dispatchTouchEvent將該事件分發DecorView,這樣,Touch事件就傳遞到了View樹中了。
Touch事件從WMS到ViewRootImpl的傳遞
下面這張圖(不是自己畫的,網上找的,Android4.4中,InputManager變成了InputManagerService,ViewRoot變成了ViewRootImpl)展示了Touch事件
從WMS(sever端)傳遞到ViewRootImpl(client端)的流程。
這裡需要特別注意的是,Touch事件從server端傳遞到client端採用的IPC方式並不是Binder,而是共享記憶體和管道,至於為什麼不採用Binder,應該是共享內存的效率更高,而管道(注意,兩個管道,分別負責不同方向的讀和寫)只負責通知是否有事件發生,傳遞的只是一個很簡單的字串,因此並不會太多地影響到IPC的效率。
上圖中,只有WMS、ViewRootImpl、InputManagerService、InputQueue是在FrameWork層實現的,其他部分都是在native層實現的,native 層的程式碼沒有細看,參考了些網上的一些資料和公司內部的一些分享,把整個流程串通了,這部分程式碼,如果時間充足,可以深入研究下。
在sever端中,InputReader和InputDispatcher是native 層的兩個線程,前者不斷地從EventHub中讀取事件(包括所有的事件,對不同的事件會做判斷處理),後者則不斷地分發InputReader讀取到的事件。而實際的分發操作時在InputPublish中進行的,它裡面儲存的有一個指向server端InputChannel端的指針,和一個指向ShareMemory(共享記憶體)的指標,當有事件要分發時,它會將事件寫入到ShareMemory中,並且傳遞一個特定的字串給InputChannel,由inutChannel將該字串寫入到管道中。在client端,一旦InputChannel從管道中讀取到有事件分發過來,便會通知InPutConsumer從ShareMemory中讀取具體的事件,並傳遞到framework層的InputQueue中。同樣,一旦事件消費完畢,client端會通過管道告訴server端,事件已經消費完畢,流程與上面的似。
大致的流程就是這樣。
另外,順便說下這裡的兩個InputChannel,這兩個InputChannel是在native 層的,他們在framework層各自有一個對應的InputChannel類,對於這兩個framework層的InputChannel,client端的是在ViewRootImpl中的setView中new出來的,但是並未做任何初始化操作(真正的初始化操作是跟server端的一起在WMS中執行的),也就是構造方法裡面為空。
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
server端的InputChannel雖然是在server端建立的,但其建立過程是在client端發起的,ViewRootImpl中有server端的Session的代理,同樣是setview方法中,通過Session代理執行server端Session的addToDisplay方法,該方法接受了client端的InputChannel方法
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
addToDisplay方法在Session類中,它會呼叫WindowManagerService的addWindow方法,而兩個InputChannel的初始化操作都是在這裡面的這這段程式碼中進行的。
if (outInputChannel != null && (attrs.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
String name = win.makeInputChannelName();
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
win.setInputChannel(inputChannels[0]);
inputChannels[1].transferTo(outInputChannel);
mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
}
這裡的InputChannel.openInputChannelPair方法會在native層建立兩個InputChannel,也就是我們上面看到的那兩個,並返回對應的framework層InputChannel的兩個物件,儲存在iputChannels陣列中,其中一個保留字server端,一個通過transferTo方法返回到client 端。這裡的InputChannel.openInputChannelPair方法和transferTo方法中都是直接呼叫來了native方法,這裡不再貼程式碼。
說了這麼多,其實就是一個Binder機制,關於Binder機制,大家自行在搜尋吧,入門的資料還是挺多的。這裡我根據原始碼畫了兩份ViewRootImpl和WMS之間通過Binder 機制進行IPC的序列圖,有興趣的可以自行研究下程式碼,只要能搞清楚Binder機制,這部分程式碼還就不難懂。
ViewRootImpl到WMS的連線,通過WMS提供給ViewRootImpl的IWinowSession成員,也就是Session在本地的代理來完成:
WMS到 ViewRootImpl的連線,通過ViewRootImpl提供給WMS的Iwindow(ViewRootImpl的內部類)來完成:
Touch事件在View樹中的分發
當Touch事件傳遞到了ViewRootImpl中後,就會在View樹中進行分發,要了解Touch事件在View樹中的分發,首先需要了解View樹的建立,這部分又可以寫成一篇單獨的博文了,具體的過程這裡不再詳說,有興趣的可以自己研究下。View樹建立完成後的結構是這樣的(圖片源自網路):
View樹的根View永遠是DecorView,它繼承自FrameLyout,其內部會有一個LinearLayout,根據Window Feather的的不同,LinearLayout內部的佈局也不同,其中每種不同佈局的xml(系統資源內部的xml佈局)內都有一個id為content的FrameLayout,這就是我們在自己的佈局所attach的父容器。
Touch事件的傳遞自然是先從ViewRootImpl傳遞到DecorView中,這個前面的第三點也說到了,因此我們這裡就從DecorView入手,開始分析Touch事件的分發。
在開始分析之前,先大致梳理下Touch事件傳遞可能涉及到的一些基礎:
1、一般情況下,每一個Touch事件,總是以ACTION_DOWN事件開始,中間穿插著一些ACTION_MOVE事件(取決於是否有手勢的移動),然後以ACTION_UP事件結束,中間還會會有onTouch、Click、LongClick等事件。
2、事件分發過程中,包括對MotionEvent事件的三種處理操作:
分發操作:dispatchTouchEvent方法,後面兩個方法都是在該方法中被呼叫的。
攔截操作:onInterceptTouchEvent方法(ViewGroup)
消費操作:onTouchEvent方法和OnTouchListener的onTouch方法,其中onTouch的優先順序高於onTouchEvent,若onTouch返回true,那麼就不會呼叫onTouchEvent方法。
3、dispatchTouchEvent分發Touch事件是自頂向下,而onTouchEvent消費事件時自底向上,onTouchEvent和onIntercepteTouchEvent都是在dispatchTouchEvent
中被呼叫的。
下面,正式進入對Touch事件在View樹中分發的分析:
首先來看DecorView(PhoneWindow的內部類)中dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
: super.dispatchTouchEvent(ev);
}
這裡的cb就是當前的Activity,Activity實現了Window.Callback介面,同時在Activity的attach方法中,建立PhoneWindow後,呼叫了
mWindow.setCallback(this)將PhoneWindow中的callback設定為當前的的Activity,因此這裡cb.dispatchTouchEvent就是Activity的
dispatchTouchEvent方法,如果前面三個條件同時成立(一般是都成立的),則呼叫Activity的dispatchTouchEvent方法進行事件的分發,
否則,直接呼叫super.dispatchTouchEvent方法,也即是FrameLayout的dispatchTouchEvent方法,其實即使呼叫了Activity的
dispatchTouchEvent方法,最終也是會呼叫到super.dispatchTouchEvent,我們可以繼續往下看Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
前面if分支不用管,這裡會呼叫PhoneWindow的superDispatchTouchEvent方法,進去看看:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
呼叫了DecorView的superDispatchTouchEvent方法,再進去看看:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
最終還是呼叫來DecorView的super.dispatchTouchEvent,也就是說,無論怎樣,DecorView的dispatchTouchEvent最終都會呼叫到自己父親FrameLayout的dispatchTouchEvent方法,而我們在FrameLayout中找不到dispatchTouchEvent方法,所以,會去執行ViewGroup的
dispatchTouchEvent方法。如果該dispatchTouchEvent返回true,說明後面有view消費掉了該事件,那就返回true,不會再去執行自身的onTouchEvent方法,否則,說明沒有view消費掉該事件,會一路回傳到Activity中,然後呼叫自己的onTouchEvent方法,該方法的實現比較簡單,如下:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
首先呼叫mWindow.shouldCloseOnTouch方法來判斷是否需要關閉視窗,如果是,則finish掉該Activity,並返回true,否則,返回false,一般情況下,是返回false的,那什麼時候回返回true呢?我們來看下Window類中的shouldCloseOnTouch方法:
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
這裡的大致意思是,如果設定了mCloseOnTouchOutside屬性為true(對應xml中的android:windowCloseOnTouchOutside屬性),且當前事件為down事件,且down事件發生在該Activity範圍之外,並且DecorView不為null,就返回true,很明顯,dialog形的Activity可能會發生這種情況。
下面需要重點來看下ViewGroup中的dispatchTouchEvent方法了:
public boolean dispatchTouchEvent(MotionEvent ev) {
//除錯用的
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//handled為返回值,表示是否有view消費了該事件。
boolean handled = false;
//是否要過濾掉該Touch事件,大致是這個意思
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//由於down事件代表一個系列事件的開始,因此如果是down事件,
//1、就清空掉以前消費事件的目標view,這裡主要指清空掉mFirstTouchTarget連結串列(儲存接受Touch事件的單連結串列,這點在後面的程式碼中會看到),並將mFirstTouchTarget置為null;
//2、重置觸控狀態,重置了disallowIntercept對應的標誌位,該變數的值決定了onInterceptTouchEvent方法是否有效,這點後面我們會看到;還有就是重置來View的mPrivateFlags標誌位,這個沒去了解具體是幹嘛用的。
一般在發生app的切換,或者ANR等情況時,程式碼會走到這裡,這一點原始碼的註釋裡也有。
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 標記是否要攔截該Touch事件,true表示攔截,false表示不攔截
final boolean intercepted;
//如果當前事件為down事件,或者可接受Touch事件的連結串列不為空,就執行if語句裡的邏輯。這裡注意,
//1、由於down事件是一個完整事件序列的的起點,因此當發生down事件時,邏輯走到這裡,還沒有找到消費down事件的view,因此mFirstTouchTarget為null,
//2、而對後面的move和up事件,如果前面的down事件被某個view消費掉了,則mFirstTouchTarget不為null。
上面兩種情況都會使程式碼進入if分支中來。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否不允許攔截,預設為false,也就是允許該方法可以通過 requestDisallowInterceptTouchEvent方法來設定
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果允許攔截,則onInterceptTouchEvent有效,根據我們覆寫的該方法的返回值來判斷是否攔截,否則,onInterceptTouchEvent無效,不對該事件進行攔截。
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 如果當前事件不是down事件,且之前在分發down事件的時候沒有找到消費down事件的目標view,也即mFirstTouchTarget為null,則直接攔截該事件。
intercepted = true;
}
// 檢查當前事件是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//儲存消費事件的目標View所對應的 TouchTarget物件
TouchTarget newTouchTarget = null;
//事件是否已經分發到了目標View中。
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果沒有被取消,並且沒有被攔截,就分發該事件,注意只有down事件才會走到這裡去分發,對於move和up事件,則會跳過這裡,直接從 mFirstTouchTarget連結串列中找到之前消耗down事件的目標View,直接將move和up事件非法給它,後面的程式碼中我們會分析到。
if (!canceled && !intercepted) {
//只有down事件會走到這裡
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//Touch事件的index,對於單點觸控,一直為0,這裡不用深究
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
//該ViewGroup中子View的個數
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//當前事件發生的位置
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//儲存該ViewGroup中子View
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
//遍歷子View,找到能消費該down事件的子View,對於型別為ViewGroup的子View,在分發的時候,會遞迴呼叫到它的dispatchTouchEvent方法繼續進行分發。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
//如果當前子View可以消費該down事件,並且該down事件發生的位置在當前子View的範圍內,則繼續執行,將down事件分發給它,否則,continue判斷下一個子View可否接受該down事件。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//判斷該能接受該down事件的child是否已經在mFirstTouchTarget連結串列中,如果在的話,說明child已經消費掉了該down事件,直接跳出迴圈。我在寫demo跟程式碼時,沒有一次走到這裡的,暫時不是很清楚,怎樣的場景下,程式碼會執行到這裡的break。
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//如果該child還沒有消費掉該down事件,就直接呼叫dispatchTransformedTouchEvent方法將該down事件傳遞給該child,該方法裡面會呼叫到child的dispatchTouchEvent方法,如果該方法返回true,則說明child消費掉了該down事件,那麼就執行if語句裡的邏輯,將child加入到mFirstTouchTarget連結串列的表頭,並且將該表頭賦值給newTouchTarget(參見addTouchTarget方法),同時 alreadyDispatchedToNewTouchTarget置為true,說明有子view消費掉了該down事件。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//如果newTouchTarget為null,並且 mFirstTouchTarget不為null,也即沒找到子View來消耗該事件,但是儲存Touch事件的連結串列不為空,則把newTouchTarget賦值為最早加進(Least Recently added)mFirstTouchTarget連結串列的target。暫時沒完全搞明白這裡的具體意思,跟程式碼都沒有走到這裡。
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//後面在處理MOVE和UP事件時,會直接根據上次的DOWN是否被消費掉來直接進行對應的處理。
if (mFirstTouchTarget == null) {
// 如果沒有子view接受該事件,則直接把當前的ViewGroup當作普通的View看待,把事件傳遞給自己(詳見dispatchTransformedTouchEvent方法,注意第三個引數傳遞的是null)。 handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 如果之前的DOWN事件被子view消費掉了,就會直接找到該子View對應的Target,將MOVE或UP事件傳遞給它們。
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { //如果該事件已經被消費掉了,則不再進行分發(該分支主要針對DOWN事件)
handled = true;
} else {
//否則,就直接將DOWN或UP事件分發給目標Target(之前消費DOWN事件的view對應的target,注意dispatchTransformedTouchEvent的第三個引數為target.child),這裡要注意的是,如果intercepted為true,也就是MOVE或UP事件被攔截了,則cancelChild為true,則會分發一次CANCLE事件(注意dispatchTransformedTouchEvent的第二個引數)。
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;
}
}
// 如果當前事件是CANCLE或UP,會呼叫resetTouchState方法,清空Touch狀態,這裡會清空mFirstTouchTarget連結串列,並將mFirstTouchTarget置為null
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
另外,畫了張ViewGroup中dispatchTouchEvent方法程式碼執行的流程圖,可以有助於大家對程式碼整體邏輯的把握(Windows上viso中畫的圖,傳到mac上就變成這樣了,重新儲存成圖片,清晰度太低,直接在PPT裡面截出來了,湊合著看吧,沒太大影響)。
關於上面提到的dispatchTransformedTouchEvent方法,這裡就不多分析了,感興趣可以自己分析下,另外,ViewGroup中沒有複寫onTouchEvent方法。
下面重點看下View中的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
很明顯,會先判斷該View有沒有繫結OnTouchListener監聽器,如果繫結了,並且複寫了其中的onTouch方法,如果onTouch方法返回了true,那麼Touch事件就被消費掉了,後面的onTouchEvent方法就不會得到執行,而如果沒有被消費掉,才會執行到onTouchEvent方法,根據其返回值來判定Touch時間是否被消費掉。這裡重點關注消費Touch事件的先後順序:onTouch先於onTouchEvent。
下面就關鍵來看下View中的onTouchEvent方法了。
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true);
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
這裡其實沒太多要說的,重點關注:
1、onClick和onLongClick執行的時機:onClick時在UP事件中執行的,onLongClick實在Down事件中執行的,只是如果在Down事件中已經執行了onLongClick的話,則mHasPerformedLongPress變數會被置為true,這樣在UP事件中,就會把onClick的回撥remove掉,就不會再執行onClick了。
2、只要該View是clickable的,就一定會消費掉Touch事件,只是,如果該View是Disable的話,雖然消費掉了Touch事件,但是不做任何處理。
另外,有一點大致說下:
原始碼的前面部分有一個mTouchDelegate變數(預設為null),如果它不為null的話,會將Touch事件分發給它。具體的意思是這樣的,假設有兩個檢視v1和touchDelegate1,它們的佈局相互之間不重疊;如果設定了v1.setTouchDelegate(touchDelegate1)的話,v1的觸控事件就會分發給touchDelegate1中的view(TouchDelegate中有一個view變數)。
為了便於整體上對原始碼流程的把握,這裡同樣畫了一個流程圖
最後,關於整個Touch事件在View樹中的傳遞流程,同樣畫了張流程圖,看起來會更直觀,有助於對整體流程的把控:
以上流程圖中有些地方文字有錯位,應該不影響對流程的整體理解和把握。
其實相對來說,事件的分發處理屬於Android中比較基礎的知識點,但想把整個流程完整地串通,還是要花些時間的,這篇文章在10月份的時候就想寫了,但是工作後,寫部落格的時間越來越少,人也變得越來越懶了。。。整篇文章斷斷續續堅持著寫下來還是挺費勁的。
好了,不多說了,下篇文章,將通過一個Demo,結合8種不同的情況,對Touch事件在View樹中傳遞時原始碼的執行情況做一個詳細的分析。
知乎上可以看到一個不一樣的我,歡迎關注:蘭亭風雨的知乎首頁
一直想做個公眾號,但是考慮到在手機端看程式碼的使用者體驗確實太差,而我又比較喜歡正能量的東東,最後倒騰了一個雞湯號,每日一篇正能量好文,同時各種網際網路資訊爆料,盡在其中。。。歡迎掃碼關注。
相關文章
- Android View 事件傳遞機制剖析AndroidView事件
- Android Touch事件傳遞機制通俗講解Android事件
- Android學習之 Touch事件傳遞機制Android事件
- Android事件傳遞機制Android事件
- Android TouchEvent事件傳遞機制Android事件
- Android onTouch事件傳遞機制Android事件
- Android觸控事件傳遞機制Android事件
- Android10_原理機制系列_事件傳遞機制Android事件
- View 事件傳遞體系知識梳理(1) 事件分發機制View事件
- 一步步探索學習Android Touch事件分發傳遞機制(三)Android事件
- 一步步探索學習Android Touch事件分發傳遞機制(二)Android事件
- 一步步探索學習Android Touch事件分發傳遞機制(一)Android事件
- 【朝花夕拾】Android自定義View篇之(五)Android事件分發及傳遞機制AndroidView事件
- Android中觸控事件的傳遞機制Android事件
- 初識Android觸控事件傳遞機制Android事件
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- 快速理解android事件傳遞攔截機制概念Android事件
- Android從原始碼角度剖析View事件分發機制Android原始碼View事件
- Android事件分發:從原始碼角度分析View事件分發機制Android事件原始碼View
- Android的Touch事件處理機制介紹Android事件
- ViewGroup/View的事件分發機制(1)(Touch,down,move,up)View事件
- ViewGroup/View的事件分發機制(2)(Touch,down,move,up)View事件
- ViewGroup/View的事件分發機制(3)(Touch,down,move,up)View事件
- Android全面解析之Window機制Android
- Android View 的事件體系 -- 事件分發機制AndroidView事件
- View事件機制分析View事件
- Android View事件機制 21問21答AndroidView事件
- Android Handler訊息傳遞機制:圖文解析工作原理Android
- Android全面解析之Context機制AndroidContext
- 用一張圖告訴你Android中的事件傳遞機制Android事件
- Android事件分發機制解析Android事件
- Android 開源專案原始碼解析 -->公共技術點之 View 事件傳遞(一)Android原始碼View事件
- 【Android原始碼】View的事件分發機制Android原始碼View事件
- View事件分發機制View事件
- 從事件驅動程式設計模型分析Handler訊息傳遞機制事件程式設計模型
- 安卓5.1原始碼解析 : ListView解析 從繪製,重新整理機制到Item的回收機制全面講解安卓原始碼View
- 10分鐘理解 Android View 事件分發機制AndroidView事件
- 圖片會說話系列之Android事件的分發傳遞機制Android事件