本篇和觸摸事件的分發(fā)(View篇)將側(cè)重于Android源碼的分析。略顯枯燥,但Read the fucking code的code不就是這樣嗎?
本文代碼基于API15分析,而不是最新的API23。因API23中多出的代碼和本文無(wú)關(guān)。
為減少篇幅完整地代碼注釋會(huì)在文末給出鏈接。
關(guān)鍵的成員變量
觸摸目標(biāo)
TouchTarget
是ViewGroup
的一個(gè)靜態(tài)內(nèi)部類。描述一個(gè)被觸摸的 childView 和他所捕獲的手指的ids。
private static final class TouchTarget {
public View child;//被觸摸到得child
public int pointerIdBits;//由該目標(biāo)捕獲的所有的手指的IDS 的結(jié)合位掩碼
public TouchTarget next;//用于指向鏈表中的下一個(gè)TouchTarget
}
記錄到的全是子View的信息,在處理向子View發(fā)送事件的邏輯時(shí)使用
觸摸目標(biāo)鏈表
為了有條理得向子View分發(fā)事件,ViewGroup需要記錄所有用戶觸摸到的觸摸目標(biāo)信息。在經(jīng)過(guò)一系列判斷邏輯后,向其中的觸摸目標(biāo)分發(fā)事件。
這里是通過(guò)定義一個(gè)TouchTarget
類型的成員變量mFirstTouchTarget
來(lái)實(shí)現(xiàn)的,其記錄了第一個(gè)觸摸目標(biāo),是觸摸目標(biāo)鏈表的頭。通過(guò)它(的next
),可以找到鏈表中所有的觸摸目標(biāo)。
private TouchTarget mFirstTouchTarget;
ViewGroup.dispatchTouchEvent()
ViewGroup中關(guān)于事件的分發(fā)是通過(guò)重寫(xiě)dispatchTouchEvent
實(shí)現(xiàn)的。這部分是事件分發(fā)過(guò)程中最為復(fù)雜和難的地方。充分了解了這部分代碼。將再也沒(méi)有難點(diǎn)來(lái)阻擋你理解觸摸事件的分發(fā)了。
ViewGroup中的dispatchTouchEvent()方法非常復(fù)雜,不了解整體設(shè)計(jì)思路,直接閱讀將會(huì)一頭霧水,云里霧里。所以,這里先把ViewGroup的的dispatchTouchEvent()用以下流程圖歸納如下:
其中比較關(guān)鍵的有以下幾點(diǎn):
- 觸摸事件的安全策略
- 處理最初的DOWN事件
- 檢查事件的攔截情況
- 檢查是否取消
- 根據(jù)需要為DOWN事件更新觸摸目標(biāo)鏈表
- 分發(fā)觸摸事件到目標(biāo)View
- 根據(jù)需要,為UP、CANCEL事件更新觸摸目標(biāo)鏈表
其中,除了第一條相對(duì)分發(fā)流程比較獨(dú)立外,其余都標(biāo)有數(shù)字序號(hào),并在圖中用藍(lán)色標(biāo)注出來(lái)了。
接下來(lái),我們分別詳細(xì)講每一個(gè)點(diǎn)。
〇、觸摸事件的安全策略
根據(jù)用戶體驗(yàn)來(lái)講,用戶只會(huì)嘗試去點(diǎn)擊可以直接看到的View(或ViewGroup),所以Google據(jù)此為觸摸事件的分發(fā)制定了一個(gè)安全策略:
如果某View不處于頂部,并且View設(shè)置的屬性是該View不在頂部時(shí)不響應(yīng)觸摸事件,則不分發(fā)該事件。
不滿足安全策略需要同時(shí)滿足兩個(gè)條件:
- 配置設(shè)定被遮擋時(shí)需要過(guò)濾觸摸事件(mViewFlags包含F(xiàn)ILTER_TOUCHES_WHEN_OBSCURED)
- 觸摸事件確實(shí)被遮擋(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
若不滿足安全策略,onFilterTouchEventForSecurity(MotionEvent event)
方法返回false,從上文流程圖可以看到,這種情況會(huì)放棄接下來(lái)的所有分發(fā)操作。
/**
* 依據(jù)安全策略,過(guò)濾觸摸事件。
* @param event The motion event to be filtered. 需要被過(guò)濾的觸摸事件。
* @return True if the event should be dispatched, false if the event should be dropped.
* 該事件需要被分發(fā),則返回true。該事件需要被丟棄,則返回false。
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
一、處理最初的DOWN事件,重置ViewGroup狀態(tài)
從Android觸摸事件--MotionEvent一文我們可以知道:DOWN類型的事件是一系列觸摸事件的開(kāi)始。
- 從用戶角度講:每次點(diǎn)擊屏幕時(shí),必然是首先觸發(fā)一個(gè)DOWN事件。
- 從代碼角度講:ViewGroup最先接收到得必然是DOWN事件,之后才會(huì)陸續(xù)接收到MOVE、UP或CANCEL等類型的事件。
當(dāng)上一次觸摸結(jié)束后,ViewGroup的某些狀態(tài)可能已經(jīng)發(fā)生了變化,比如ViewGroup的成員變量mFirstTouchTarget
已經(jīng)記錄了一些值。
這時(shí),做了兩個(gè)動(dòng)作:
-
取消并清空觸摸目標(biāo)鏈表。
cancelAndClearTouchTargets(MotionEvent event)
/**
* 取消并清空所有的的觸摸目標(biāo)
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) { //如果mFirstTouchTarget鏈表不為空,則清空該鏈表
boolean syntheticEvent = false; //是否是我們?nèi)藶楹铣傻氖录? if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);//人為合成一個(gè) ACTION_CANCEL 事件
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
//從cancelAndClearTouchTargets的調(diào)用關(guān)系,我們可以發(fā)現(xiàn),這里發(fā)送出去的事件只可能是兩種:
// 1、處理 DOWN 事件時(shí)的DOWN事件
// 2、當(dāng)該ViewGroup離開(kāi)屏幕(Window)時(shí),發(fā)送上面人為合成的ACTION_CANCEL 消息。
// 但因?yàn)榈诙€(gè)參數(shù)為true,無(wú)論是哪種事件,最終都會(huì)被轉(zhuǎn)化為取消事件。
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
根據(jù)Android中ViewGroup源碼,該方法除了在這里之外,還會(huì)在ViewGroup的dispatchDetachedFromWindow()方法中被調(diào)用。這部分邏輯在流程圖中特別標(biāo)示出來(lái)了。在分發(fā)觸摸事件的情況下地邏輯就簡(jiǎn)單了不少。
1)遍歷觸摸目標(biāo)列表,將鏈表中的所有子View的 CANCEL_NEXT_UP_EVENT 標(biāo)志全部重置
2)將取消事件傳遞給鏈表中所有的子View。(這里事件雖未DOWN事件,但dispatchTransformedTouchEvent方法第二個(gè)參數(shù)為true,最終都會(huì)被轉(zhuǎn)化為ACTION_CANCEL事件)
3)清空觸摸鏈表 clearTouchTargets()
。
-
重置所有觸摸狀態(tài)來(lái)為一個(gè)新的循環(huán)做準(zhǔn)備。
resetTouchState();
private void resetTouchState() {
clearTouchTargets();//清空觸摸鏈表
resetCancelNextUpFlag(this);//重置CANCEL_NEXT_UP_EVENT 標(biāo)志
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置 FLAG_DISALLOW_INTERCEPT 標(biāo)志
}
這里代碼非常簡(jiǎn)單,就是清空觸摸鏈表、重置本ViewGroup實(shí)例的 CANCEL_NEXT_UP_EVENT 標(biāo)志、重置 FLAG_DISALLOW_INTERCEPT標(biāo)志。
和cancelAndClearTouchTargets()方法主要處理觸摸鏈表中的子View不同,該方法主要是針對(duì)ViewGroup實(shí)例自身的一些處理。
二、檢查事件的攔截情況
APP開(kāi)發(fā)工程師在開(kāi)發(fā)程序時(shí),可以對(duì)ViewGroup是否攔截事件做的限定有:
1、是否允許攔截觸摸事件;由 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 來(lái)設(shè)定
2、是否要攔截某個(gè)觸摸事件;由onInterceptTouchEvent(MotionEvent ev)返回值來(lái)決定
而ViewGroup內(nèi)部是如何處理這些限定呢?
我們結(jié)合代碼來(lái)看一下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
****代碼從略 ****
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//允許攔截
intercepted = onInterceptTouchEvent(ev);//根據(jù)onInterceptTouchEvent(dev)決定是否攔截
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//不允許攔截
}
} else {//不是down事件并且mFirstTouchTarget==null也沒(méi)用與之對(duì)應(yīng)的觸摸目標(biāo)
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 不存在觸摸目標(biāo),并且該事件不是down事件,那么就繼續(xù)攔截觸摸事件。
//(比如一個(gè)空的ViewGroup,則攔截觸摸事件,通過(guò)自己的touchEvent處理)。
// (又比如雖ViewGroup不為空,但觸摸事件并沒(méi)發(fā)生在任何子View上)。
intercepted = true;
}
****代碼從略 ****
}
在兩種情況下,ViewGroup需要根據(jù)APP工程師的限定來(lái)決定事件是否被攔截:
- 當(dāng)發(fā)生down事件時(shí)
down事件是一個(gè)完整事件序列的的起點(diǎn),當(dāng)發(fā)生down事件時(shí),這時(shí)還不知道是否有子View可消費(fèi)該事件(此時(shí)mFirstTouchTarget已經(jīng)被重置為null),必須根據(jù)APP工程師對(duì)ViewGroup的限定,方知是否需要攔截該事件。 - mFirstTouchTarget 不為null時(shí)
這意味已經(jīng)(靠該序列的起點(diǎn)down事件)找到要消費(fèi)觸摸事件的目標(biāo)了,那么肯定不會(huì)是down事件了,而是move、up等類型的事件。這時(shí),我們也需要根據(jù)APP工程師對(duì)ViewGroup的限定來(lái)決定是否攔截這種類型(move、up等)的事件。
從代碼可知:只有在允許攔截并且onInterceptTouchEvent(MotionEvent ev)返回true時(shí),才會(huì)對(duì)觸摸事件攔截。
在以下情況下,觸摸事件默認(rèn)需要交給ViewGroup自己來(lái)處理,這時(shí),我們可以當(dāng)做事件被攔截了來(lái)處理:
- 既不是down事件,此前也沒(méi)有根據(jù)事件序列的down事件找到處理目標(biāo)。(沒(méi)有能夠找到處理目標(biāo)的move、up事件,攔截并交給ViewGroup自身來(lái)處理)。
比如說(shuō)一個(gè)空的Layout布局;Layout布局不為空,但用戶從未點(diǎn)擊到任何子View上。
三、檢查事件是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
很簡(jiǎn)單兩種情況:
- 當(dāng)前的View或ViewGroup要被從父View中detach時(shí),PFLAG_CANCEL_NEXT_UP_EVENT就會(huì)被設(shè)為true;此時(shí),resetCancelNextUpFlag(this)返回true,canceled被賦值為true,它就不再接受觸摸事情。
- 觸摸事件本身就是MotionEvent.ACTION_CANCEL類型的事件。
四、根據(jù)需要,為down事件更新觸摸目標(biāo)鏈表
- 獲取到該觸摸事件所對(duì)應(yīng)手指ID,從觸摸目標(biāo)鏈表中清空與之對(duì)應(yīng)的所有觸摸目標(biāo)。
- 遍歷子View,直到找到即可接收觸摸事件,觸摸事件的坐標(biāo)又坐落在其坐標(biāo)范圍內(nèi)的childView;找到了就繼續(xù),找不到結(jié)束循環(huán)。
- 嘗試從已有的觸摸目標(biāo)鏈表中找到與該childView對(duì)應(yīng)的觸摸目標(biāo)實(shí)例,找到即結(jié)束。
- 沒(méi)有在已有鏈表中找到對(duì)應(yīng)的觸摸目標(biāo)實(shí)例,就把該事件發(fā)送給childView去處理,并生成一個(gè)觸摸目標(biāo)實(shí)例加入到觸摸目標(biāo)鏈表中。
- 找不到newTouchTarget,并且觸摸目標(biāo)鏈表不為空時(shí),將newTouchTarget指向觸摸目標(biāo)鏈表的最初的target去處理
?1:第一個(gè)手指按下(單點(diǎn)觸摸)時(shí)mFirstTouchTarget鏈表被清空,肯定找不到
2:多點(diǎn)觸摸的后續(xù)手指按下時(shí),從前面手指按下時(shí)產(chǎn)生的mFirstTouchTarget鏈表中尋找
五、分發(fā)觸摸事件到目標(biāo)。
如果觸摸目標(biāo)鏈表為空,直接把該ViewGroup當(dāng)成一個(gè)普通的View來(lái)處理,
如果觸摸目標(biāo)鏈表不為空,則遍歷鏈表,將事件分發(fā)給觸摸目標(biāo)對(duì)應(yīng)的childView中去。若某childView事件接收到了CANCEL事件,就從鏈表中移出該觸摸目標(biāo)。
六、處理CANCEL事件和手指抬起事件
-
該ViewGroup接收到了ACTION_CANCEL 事件,或者是最后一個(gè)手指抬起時(shí)
重置所有觸摸狀態(tài)來(lái)為一個(gè)新的循環(huán)做準(zhǔn)備:resetTouchState();
-
多點(diǎn)觸摸時(shí),抬起了一根手指(非最后一個(gè))
從觸摸鏈表中移出所有的與該手指有關(guān)的TouchTarget,不再考慮任何與該手指相關(guān)的操作(因?yàn)橐呀?jīng)抬起)。final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove);
http://blog.csdn.net/ns_code/article/details/49848801
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/
http://blog.csdn.net/yanbober/article/details/45912661
http://blog.csdn.net/lfdfhl/article/details/42241253
http://www.cnblogs.com/hi0xcc/p/5583791.html
觸摸事件的安全策略
/**
* Filter the touch event to apply security policies.
* 依據(jù)安全策略,過(guò)濾觸摸事件。
* 安全策略:
* ①:配置設(shè)定被遮擋時(shí)需要過(guò)濾觸摸事件(mViewFlags包含F(xiàn)ILTER_TOUCHES_WHEN_OBSCURED)
* ②:觸摸事件確實(shí)被遮擋(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
* @param event The motion event to be filtered. 需要被過(guò)濾的觸摸事件。
* @return True if the event should be dispatched, false if the event should be dropped.
* 該事件需要被分發(fā),則返回true。該事件需要被丟棄,則返回false。
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
觸發(fā)ACTION_CANCEL事件
運(yùn)行上面兩個(gè)例子,如果沒(méi)什么差錯(cuò)的話,你是不會(huì)看到ACTION_CANCEL事件的,為什么呢?
要觸發(fā)ACTION_CANCEL,就先得了解一個(gè)類ViewGroup,ViewGroup是一個(gè)放置其他views(子view)的特殊view,它是布局類(*Layout)、視圖容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基類。
也就是說(shuō)ViewGroup一般是做為父視圖來(lái)容納、管理其他子視圖的。既然管理,在用戶手勢(shì)操作過(guò)程中,就會(huì)存在父視圖不希望子視圖響應(yīng)用戶手勢(shì)操作的情況。Android提供了一個(gè)函數(shù)public boolean onInterceptTouchEvent (MotionEvent ev),在用戶手勢(shì)操作時(shí),系統(tǒng)先調(diào)用父視圖(一個(gè)繼承自ViewGroup的類)的這個(gè)函數(shù),來(lái)決定當(dāng)前手勢(shì)操作是由父視圖還是子視圖來(lái)響應(yīng)、處理。我們仔細(xì)看看這個(gè)函數(shù)名,函數(shù)名中有一個(gè)單詞intercept,經(jīng)過(guò)查詞典,這個(gè)單詞的中文意思是攔截。在用戶的一個(gè)完整手勢(shì)操作過(guò)程中(起自ACTION_DOWN,終于ACTION_UP),對(duì)于每一次的MotionEvent``Android都會(huì)調(diào)用該函數(shù),向父視圖查詢是否攔截當(dāng)前MotionEvent,如果父視圖返回false:不攔截,則系統(tǒng)會(huì)調(diào)用子視圖的onTouchEvent函數(shù);如果父視圖返回true:攔截,則系統(tǒng)調(diào)用父視圖的onTouchEvent。等等,有人不禁要問(wèn)了,如果在這個(gè)完整手勢(shì)操作過(guò)程中,父視圖初期返回false、后期返回true會(huì)是一個(gè)什么樣的情況呢(搗亂的來(lái)了)?這個(gè)嘛,是這個(gè)樣子的,一開(kāi)始返回false,毫無(wú)疑問(wèn),子視圖會(huì)被調(diào)用onTouchEvent,但凡父視圖在函數(shù)onInterceptTouch中有一次返回了true,那這一完整手勢(shì)操作內(nèi)所有后續(xù)的MotionEvent都會(huì)調(diào)用父視圖的onTouchEvent,即使父視圖后期反悔而改成返回false也不行(沒(méi)有后悔藥)。在這種父視圖先返回false,后返回true的情況下,子視圖收不到后續(xù)的事件,而只是在父視圖由返回false改成返回true(攔截)的時(shí)候收到ACTION_CANCEL事件。
我們可以得到的結(jié)論
- 對(duì)于一個(gè)事件序列,當(dāng)
ACTION_DOWN
事件被成功攔截時(shí),那么對(duì)于剩下的一系列事件也會(huì)被攔截,并且不會(huì)再次執(zhí)行onInterceptTouchEvent方法 - 觸摸目標(biāo)鏈表只有在
ACTION_DOWN
或ACTION_POINTER_DOWN
事件時(shí)才可能被新增,所以,如果一系列事件的后續(xù)事件(ACTION_MOVE、ACTION_UP等)要想會(huì)被處理,這一系列事件的
疑問(wèn)
- 在處理最初子View事件時(shí),是否會(huì)對(duì)觸摸鏈表中的子View都傳遞DOWN事件。
其他
本篇在將來(lái)的某天會(huì)有更新。