在Android尤其是手機(jī)端的開(kāi)發(fā)中,很多情況下涉及到點(diǎn)擊事件,或者說(shuō)是觸摸事件的特殊處理。比如滑動(dòng)沖突等,因此熟知Android事件傳遞的流程就顯得格外重要。今天老衲就帶大家理一理Android事件傳遞的相關(guān)細(xì)節(jié)。
方法介紹
事件序列
是指當(dāng)手指接觸屏幕至手指離開(kāi)屏幕是所產(chǎn)生的一系列Down-->Move-->Up事件。
事件傳遞涉及到三個(gè)方法和兩個(gè)監(jiān)聽(tīng)(onClick也算,原因在源碼分析中可以看到):
分發(fā)事件
//@return True 表示事件被該View處理
public boolean dispatchTouchEvent(MotionEvent ev)
如果事件能夠傳遞到該View(或activity)則該方法一定會(huì)被調(diào)用。
攔截事件
//@return True 表示事件被該View攔截
public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法的內(nèi)部調(diào)用,同一事件序列下,該方法只執(zhí)行一次
處理事件
//@return True 表示事件被該View消耗
public boolean onTouchEvent(MotionEvent event)
子元素用來(lái)干預(yù)父元素的事件分發(fā)過(guò)程,ACTION_DOWN除外
public void requestDisallowInterceptTouchEvent
OnTouchListener及OnClickListener
public void onClick(View v)
public boolean onTouch(View v, MotionEvent event)
- OnTouchListener內(nèi)回調(diào)的優(yōu)先級(jí)高于onTouchEvent,并且會(huì)根據(jù)Listener返回值來(lái)影響onTouchEvent方法的執(zhí)行。
- OnClickListener回調(diào)的執(zhí)行是在OnTouchEvent方法內(nèi),根據(jù)UP,DOWN事件的時(shí)長(zhǎng)來(lái)判斷是否需要執(zhí)行click方法(前提是View可以點(diǎn)擊)
事件傳遞的偽代碼
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
//如果View需要攔截該事件,則事件處理交給onTouchEvent
if (onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
//如果View不需要攔截事件,則事件交給子View來(lái)繼續(xù)該判斷。直至事件被消耗
result = child.dispatchTouchEvent(ev);
}
return result;
}
事件分發(fā)流程的源碼分析
事件分發(fā)的流程為activity--->window--->View,
Step1. activity-->window
public boolean dispatchTouchEvent(MotionEvent ev) {
//空方法,當(dāng)需要知道用戶與設(shè)備在互動(dòng)時(shí)調(diào)用,可重寫(xiě)
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//通過(guò)window將事件傳遞給View樹(shù)來(lái)處理,當(dāng)返回true(View消耗了)則事件傳遞結(jié)束
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果View并未處理,則交給activity的onTouchEvent來(lái)處理。
return onTouchEvent(ev);
}
Step2. window-->view
window是一個(gè)抽象類,superDispatchTouchEvent方法的具體實(shí)現(xiàn)是在phoneWindow中。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
//直接調(diào)用了DecorView的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
//設(shè)置title或actionBar以及其他style樣式的邏輯
...
}
接下來(lái),我們需要看一下DecorView內(nèi)部的方法實(shí)現(xiàn)邏輯。如下代碼,它直接又扔給了super的dispatchTouchEvent。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
...
}
這個(gè)super就是FrameLayout。我們都知道DecorView其實(shí)就是一個(gè)FrameLayout,我們?cè)陂_(kāi)發(fā)過(guò)程中寫(xiě)的所以得布局文件,都會(huì)被扔到這個(gè)FrameLayout中來(lái)展示,接下來(lái)我們就開(kāi)始了本文的重中之重,onTouchEvent事件的分析了。
Step3. View內(nèi)部的源碼解析
Step3.1 View的dispatchTouchEvent邏輯
我們首先看一下View內(nèi)部的事件分發(fā)邏輯,相對(duì)于ViewGroup,View的事件分發(fā)更簡(jiǎn)單。
public boolean dispatchTouchEvent(MotionEvent event) {
// 焦點(diǎn)處理
// 滑動(dòng)處理
if (onFilterTouchEventForSecurity(event)) {
...
//View監(jiān)聽(tīng)的處理,如果有onTouchListener,則執(zhí)行onTouch事件,
//然后,根據(jù)onTouch事件的返回結(jié)果來(lái)判斷是否要執(zhí)行View的onTouchEvent事件。
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
View內(nèi)的事件分發(fā)邏輯非常清晰:
- OnTouchListener是否存在,如果存在則執(zhí)行onTouch方法
- 執(zhí)行onTouchEvent方法。
其他就是一些焦點(diǎn)和滑動(dòng)情況下的特殊處理。本文不做深究。
Step3.2 ViewGroup的dispatchTouchEvent邏輯
接下來(lái),我們來(lái)看下ViewGroup內(nèi)部的事件分發(fā)邏輯
Step3.2.1 ViewGroup內(nèi)部的攔截邏輯
// 事件傳遞隊(duì)列(View的嵌套隊(duì)列)中首個(gè)消耗事件序列的目標(biāo)(View)
private TouchTarget mFirstTouchTarget;
public boolean dispatchTouchEvent(MotionEvent ev) {
//焦點(diǎn)處理
//新的點(diǎn)擊序列的初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {
//當(dāng)開(kāi)始一個(gè)新的touch事件序列時(shí),先丟棄之前事件產(chǎn)生的狀態(tài)
//因?yàn)閍pp的切換,ANR或者其他狀態(tài)的改變會(huì)導(dǎo)致Android丟掉之前手勢(shì)產(chǎn)生的UP及CANCLE事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//檢查攔截設(shè)置
final boolean intercepted;
// 重點(diǎn)1.
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//重點(diǎn)2.
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//根據(jù)onInterceptTouchEvent的返回值確定是否攔截該事件序列
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
// restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
}
重點(diǎn)1:“或” 的判斷邏輯,前半部分用來(lái)標(biāo)識(shí)事件類型(DOWN,MOVE或者UP),后半部分是用來(lái)判斷之前是否有接收了該事件序列中的View,如果任意為true,則接下來(lái)該ViewGroup的onInterceptTouchEvent方法不會(huì)再被調(diào)用,該事件序列中剩余的事件都交給mFirstTouchTarget處理。
重點(diǎn)2:之前提到過(guò),子View可以通過(guò)requestDisallowInterceptTouchEvent影響父View的事件分發(fā)。當(dāng)FLAG_DISALLOW_INTERCEPT被子View修改以后,父View將只會(huì)攔截DOWN事件,而不會(huì)攔截其他事件。至于原因可以在resetTouchState方法中找到。
Step3.2.2ViewGroup內(nèi)部的事件分發(fā)邏輯
//最近一次處理事件的View的索引
int mLastTouchDownIndex
//子View個(gè)數(shù)
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//獲取事件坐標(biāo)
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 找到一個(gè)能接受事件的子View
// 從外向內(nèi)掃描子View,根據(jù)Z軸以及繪制的順序生成的View的list
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//根據(jù)繪制的順序獲取index并得到View
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
...
//判斷是否可以接收事件
//1.View可見(jiàn)
//2.坐標(biāo)在View的區(qū)域內(nèi)
if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
//重點(diǎn)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 子View希望接收在它范圍內(nèi)的事件
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
//根據(jù)childindex找到View集合中的原始索引下標(biāo)
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
...
}
//事件沒(méi)有被能夠獲取到焦點(diǎn)的對(duì)象處理,清除flag,然后分發(fā)到所有的子View
ev.setTargetAccessibilityFocus(false);
}
//清理預(yù)排序的View集合
if (preorderedList != null)
preorderedList.clear();
}
上述代碼標(biāo)注重點(diǎn)的位置,它的具體實(shí)現(xiàn)如下,有子View則調(diào)用子View的dispatchTouchEvent,沒(méi)有則調(diào)用父類的的dispatchTouchEvent方法,如果子View依然是一個(gè)ViewGroup則遞歸,直至最后的View為止。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
// CANCLE事件的特殊處理
...
// 執(zhí)行必要的轉(zhuǎn)換和分發(fā)
if (child == null) {
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);
}
...
}
我們?cè)谝婚_(kāi)始分析了View和ViewGroup的dispatchTouchEvent方法,里面都有調(diào)用onTouchEvent事件的邏輯,根據(jù)該方法的調(diào)用結(jié)果來(lái)確定該事件是否被消耗,一旦被消耗,則dispatchTransformedTouchEvent的返回值為true,此時(shí),ViewGourp的dispatchTouchEvent會(huì)調(diào)出遍歷子view的for循環(huán),進(jìn)入下一事件的分發(fā)過(guò)程。
Step4. onTouchEvent事件的處理邏輯
public boolean onTouchEvent(MotionEvent event) {
//獲取事件的坐標(biāo)及ACTION
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//一個(gè)可點(diǎn)擊的disabled的View依然可以消耗事件,只是不會(huì)做出響應(yīng)
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//重點(diǎn)1.
if (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
{
switch (action) {
case MotionEvent.ACTION_UP:
//如果View可以獲取焦點(diǎn),則首先讓該View獲取焦點(diǎn)
//這個(gè)View的按下?tīng)顟B(tài)即將被釋放,修改樣式(selector)及動(dòng)畫(huà)
if (!post(mPerformClick)) {
//重點(diǎn)2,執(zhí)行點(diǎn)擊事件
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
...
//重點(diǎn)3
checkForLongClick(0, x, y);
break;
}
重點(diǎn)1 : 只要View的CLICKABLE或者LONG_CLICKABLE任意一個(gè)屬性為true,那就會(huì)消耗該事件
重點(diǎn)2 : 在onTouchEvent方法會(huì)間接調(diào)用OnClickListener方法的回調(diào)。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
...
//如果執(zhí)行了點(diǎn)擊事件,則表示該事件被消耗了。
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;
}
重點(diǎn)3 : 在ACTION_DOWN的判斷邏輯里會(huì)進(jìn)行長(zhǎng)按的校驗(yàn),如果判斷不是長(zhǎng)按事件則會(huì)調(diào)用UP分支的performClick方法
事件傳遞所涉及方法的執(zhí)行順序
以下日志只涉及DOWN和UP兩種事件
情景1
多層嵌套,無(wú)任何攔截,子View不處理事件
//activity分發(fā)開(kāi)始
MainActivity---->dispatchTouchEvent start
//各層View嘗試攔截
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
//如果有Listener則先執(zhí)行監(jiān)聽(tīng)的回調(diào)
OnTouchListener---->處理事件
//最終執(zhí)行onTouchEvent
MyImageView--->onTouchEvent
//--------------------------------事件回傳
MyImageView--->dispatchTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
//回傳到activity然后結(jié)束
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end
//該事件序列的剩余事件無(wú)需再次層層傳遞,直接在activity中處理
MainActivity---->dispatchTouchEvent start
MainActivity---->onTouchEventfalse
MainActivity---->dispatchTouchEvent end
此情境下情況下,首先從activity開(kāi)始,由外到內(nèi)傳遞給View的onTouchEvent,然后再由內(nèi)到外傳遞給activity的onTouchEvent,最終消耗掉此事件。這里的事件是指DOWN事件,因?yàn)闆](méi)有子View來(lái)處理該DOWN事件,所以最終由activity出面處理,那么接下來(lái)的事件序列也沒(méi)有必要繼續(xù)向內(nèi)傳遞了,可以直接在activity中處理了。
舉個(gè)栗子,CTO吩咐技術(shù)總監(jiān)一個(gè)任務(wù),技術(shù)總監(jiān)交代給你,但是你沒(méi)有完成又扔給總監(jiān),總監(jiān)也沒(méi)完成又扔給CTO,此時(shí)接下來(lái)的任務(wù)CTO就會(huì)自己去處理而不是交給下級(jí)。
情景2
多層嵌套,無(wú)任何攔截,子View處理事件
//activity分發(fā)開(kāi)始
MainActivity---->dispatchTouchEvent start
//各層View嘗試攔截
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
//監(jiān)聽(tīng)處理
OnTouchListener---->處理事件
//事件最終被消耗
MyImageView--->onTouchEvent
//回調(diào)各層級(jí)的dispatchTouchEvent
MyImageView--->dispatchTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end
//開(kāi)始UP事件的分發(fā)
MainActivity---->dispatchTouchEvent start
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
OnTouchListener---->處理事件
MyImageView--->onTouchEvent
MyImageView--->dispatchTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end
相較于上種情況,本次的事件傳遞多了UP事件的傳遞。
再舉個(gè)栗子,CTO吩咐技術(shù)總監(jiān)一個(gè)任務(wù),技術(shù)總監(jiān)交代給你,你完美的完成了,接下來(lái)的任務(wù)CTO會(huì)繼續(xù)交托給你執(zhí)行。
情景3
多層嵌套,無(wú)攔截,但是dispatchTouchEvent與onTouchEvent聲明的行為不一致(返回結(jié)果不同)
一般來(lái)說(shuō),我們會(huì)在dispatchTouchEvent方法中聲明說(shuō)事件序列由該View處理,而在onTouchEvent中確定是否真正處理了該事件序列,所以基本來(lái)說(shuō)他們是一致的,但是假如不一致,那么就會(huì)出現(xiàn)如下執(zhí)行順序
3.1
聲明要處理,但是實(shí)際上沒(méi)有處理,則該事件會(huì)被吞噬
dispatch true onTouchEvent false
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end :
3.2
聲明不處理,但是實(shí)際上處理了,則該事件會(huì)當(dāng)做沒(méi)人處理回傳給activity中,并且后續(xù)的事件也不會(huì)再向下傳遞了。
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEventtrue
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end :
MainActivity---->dispatchTouchEvent start :
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end :
情景4
多層嵌套,有攔截
4.1 只攔截,不處理
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onInterceptTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end :
MainActivity---->dispatchTouchEvent start :
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end :
只攔截,不處理的話,則事件最終仍會(huì)返回給activity處理
4.1 攔截并處理
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onInterceptTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end :
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end :
此時(shí)和最內(nèi)層View處理事件的邏輯一樣。
情景5
涉及OnClickListener的處理
當(dāng)給一個(gè)View設(shè)置OnClickListener時(shí),onClick回調(diào)方法會(huì)在最后一步執(zhí)行,它的優(yōu)先級(jí)是最低的,此處就不寫(xiě)打印日志了。因?yàn)榇颂幦罩九c情景1類似,只是在最后一步執(zhí)行onClick方法而已,需要注意的是onClick回調(diào)能夠執(zhí)行的前提是,View的dispatchTouchEvent方法返回true,onTouchEvent無(wú)所謂,否則無(wú)法接收點(diǎn)擊事件回調(diào),至于原因在源碼中也可以找到。
至此,Android的事件分發(fā)就已經(jīng)介紹完了。老衲也要苦逼的開(kāi)始找工作了。祝大家工作順利。