前言
關于自定義View系列的文章阁将,好久沒有寫了潦刃。今天抽空看了下Android開發(fā)藝術探索侮措。正好看到了View的事件分發(fā)機制,所以將它寫成筆記記錄下來乖杠。
關于View的事件分發(fā)分扎,我起初是學習郭神的2篇文章。感覺其實也沒有什么胧洒。大致也就了解下畏吓。不過看完其他很多優(yōu)秀的文章和書籍后,才知道自己too young too simple卫漫。下面我們就一起來分析下Android的時間分發(fā)機制菲饼。
關于事件分發(fā)機制,其實網(wǎng)上的文章已經(jīng)有很多了列赎。我簡單的看了幾篇宏悦,發(fā)現(xiàn)寫的都很好。之所以寫這篇文章包吝,主要是記錄自己的學習過程饼煞,其次也想幫助和我一樣的的初學者更加理解與掌握,才是本篇的目的诗越。
注:本文源碼 API=25 同時本文較長砖瞧,可以先收藏再好好閱讀
概念
在學習事件分發(fā)之前我們先來了解下,什么是事件分發(fā)嚷狞。所謂點擊事件(Touch)的事件分發(fā)块促,其實就是對MotionEvent(Touch的封裝)事件的分發(fā)過程,即當一個MotionEvent產(chǎn)生以后感耙,系統(tǒng)需要把這這個事件傳遞給那個具體的View褂乍。這個傳遞的過程就是事件分發(fā)過程。
1.MotionEvent
那么MotionEvent又是什么呢即硼?
這個類就是記錄手指接觸屏幕后所產(chǎn)生的一系列的事件(也就是說我們事件分發(fā)其實就是分發(fā)MotionEvent這個對象)逃片。這個類里包含了一系列的事件。事件的類型與含義如下:
事件類型 | 具體動作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的開始) |
MotionEvent.ACTION_UP | 抬起View(與DOWN對應) |
MotionEvent.ACTION_MOVE | 滑動View |
MotionEvent.ACTION_CANCEL | 結束事件(非人為原因) |
下面列舉2個我們常見的點擊事件序列:
- 事件序列 : DOWN->UP 點擊屏幕后松開 (常見的點擊事件)
- 事件序列 : DOWN->MOVE->MOVE->...->MOVE->UP 點擊屏幕滑動一會 (常見的滑動屏幕)
用圖來概括如下:
圖片來源
2. 事件分發(fā)的順序
事件分發(fā)的順序是Activity->ViewGroup->View只酥。也就是說在默認情況下褥实。最后消費事件的都是View。雖然我們現(xiàn)在還沒有開始深入講解裂允。但是結合我們日常開發(fā)的情況我們可以想到下面這張流程圖:
這張流程圖就算我們沒有了解事件分發(fā)损离,通過我們一直的使用規(guī)則來看,也是非常容易理解的绝编。細心的小伙伴會發(fā)現(xiàn)僻澎。為什么Activity向下分發(fā)第一個就是ViewGroup貌踏,如果我們布局中只有一個簡單View控件(如TextView)呢?還記得我們在講View的繪制流程中介紹的嗎窟勃?我們布局加載中的頂級View是DecorView(繼承FrameLayout)祖乳,他本是就是一個ViewGroup。不了解的可以回頭看下這篇文章秉氧。
2. 事件分發(fā)的核心方法
在對事件分發(fā)機制概念眷昆,以及結合平時我們經(jīng)驗總結出來的原理后。下面我們就來通過源碼來去將我們的想法串聯(lián)起來汁咏。不過在看源碼之前亚斋,我們要先講下在事件分發(fā)機制中三個至關重要的方法。如下:
方法 | 作用 | 調用時刻 | 返回結果 |
---|---|---|---|
dispatchTouchEvent(MotionEvent event) | 用來進行事件分發(fā) | 在三個方法中第一個被調用攘滩。 如果事件能夠傳遞給當前View/ViewGroup,那么此方法一定會調用 | 表示是否消耗當前事件 |
onInterceptTouchEvent(MotionEvent ev) | 用來判斷是否攔截某個事件(ViewGroup有此方法帅刊,View沒有) | 在dispatchTouchEvent()方法中調用 如果當前View攔截了某個事件,那么同一事件序列將不再會調用此方法轰驳。 | 表示是否攔截當前事件 |
onTouchEvent(MotionEvent event) | 用來處理點擊事件 | 在dispatchTouchEvent()方法中調用厚掷,不消耗當前事件,那么當前View在同一事件序列中無法再接受到事件 | 表示是否消耗當前事件 |
這三個方法就是事件分發(fā)機制中的核心三個方法级解,也是我們下面在源碼中重要去分析的三個方法冒黑。他們三者之間的關系可以概述如下(注意這是一段偽代碼。在任何類中并沒有此方法勤哗。只是為了對解釋三個方法關系):
/**
* 點擊事件產(chǎn)生后
*/
// 步驟1:調用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否會消費事件
// 步驟2:判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// a. 若攔截抡爹,則將該事件交給當前View進行處理
// 即調用onTouchEvent ()方法去處理點擊事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不攔截,則將該事件傳遞到下層
// 即 下層元素的dispatchTouchEvent()就會被調用芒划,重復上述過程
// 直到點擊事件被最終處理為止
consume = child.dispatchTouchEvent (ev) ;
}
// 步驟3:最終返回通知 該事件是否被消費(接收 & 處理)
return consume;
三個方法的解釋在加上這段偽代碼冬竟,就很好理解三者的關系了:對于一個跟ViewGroup,點擊事件產(chǎn)生后民逼,首先會傳給它泵殴,這時它的dispatchTouchEvent就會被調用,開始進行事件的分發(fā)拼苍,首先會進行判斷笑诅,判斷當前ViewGroup是否進行了攔截。如果進行攔截疮鲫,那么ev(點擊事件)就會交給ViewGroup去處理吆你。不再向下傳遞。分發(fā)結束俊犯。如果沒設置攔截妇多。那么就會調用ViewGroup中所包含的子控件的dispatchTouchEvent (ev)方法,并將事件ev向下傳遞燕侠。如果子控件還是ViewGroup繼續(xù)上面的循環(huán)者祖。知道將事件最終被處理消費掉立莉。這么一看,這不正好對應了我們前面總結的流程圖嘛七问√倚颍看來我們將事件分發(fā)的大致流程已經(jīng)都搞清楚了。
源碼分析
從上面來看烂瘫,好像事件分發(fā)機制也就這些東西了。好像我們都掌握了奇适。其實不然坟比,不過如果你上面的都理解了,說你對Android事件分發(fā)機制了個整體認識嚷往,那就一點都不為過了葛账。不過事件分發(fā)還遠不止這么簡單。里面還是有很多需要注意的點和事件在分發(fā)過程中的一些規(guī)則皮仁。下面我們就從源碼的角度來一一探索籍琳。
1. Activity事件分發(fā)
上面我們說了當一個事件的產(chǎn)生首先是傳遞個Activity。由Activity來進行事件的分發(fā)贷祈。那么我就看下Activity#dispatchTouchEvent():
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列開始都是DOWN事件 = 按下事件趋急,故此處基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//實現(xiàn)屏保功能
//是一個空方法
//當此activity在棧頂時,觸屏點擊按home势誊,back呜达,menu鍵等都會觸發(fā)此方法
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//當一個點擊事件未被Activity下任何一個View接收 / 處理
//或是發(fā)生在Window邊界外的觸摸事件就會調用
return onTouchEvent(ev);
}
這個方法非常短。這里我們重點看第二個if語句粟耻。這里的getWindow().superDispatchTouchEvent(ev)點進去是Window#superDispatchTouchEvent()查近。這是一個抽象方法。不過相信看過我前面文章的小伙伴一定知道這個方法的實現(xiàn)實在PhoneWindow中挤忙。那么找到這個方法如下:
//PhoneWindow#superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
/**
* 這里調用了mDecor.superDispatchTouchEvent(event);方法霜威。這個mDecor就* 是DecorView 下面我們繼續(xù)跟蹤
*/
//DecorView#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
//DecorView繼承FrameLayout 那么他本是就是一個ViewGroup
//那么這個方法最后就會調用到ViewGroup#dispatchTouchEvent()
return super.dispatchTouchEvent(event);
}
可以看到最后Activity的分發(fā)過程最后就是將事件交給頂級DecorView去進行事件分發(fā)。然后它又會調用ViewGroup#dispatchTouchEvent()册烈。OK戈泼!到這里我們就將我們的事件由Activity->ViewGroup的傳遞。并將返回值設置成true茄厘。表示這個事件已經(jīng)被我們消耗掉了矮冬。
這里還有一點需要注意。從源碼中我們可以看到次哈,假設Activity下的所有View或者我們點擊了Window邊界以外胎署,那么就會調用Activity#onTouchEvent(ev);這個方法:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
//Window#shouldCloseOnTouch()
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
// 主要是對于處理邊界外點擊事件的判斷:是否是DOWN事件,event的坐標是否在邊界內等
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
// 返回true:說明事件在邊界外窑滞,即 消費事件
return true;
}
// 返回false:未消費(默認)
return false;
}
這部分了解即可琼牧,并不是我們的重點恢筝。
2. ViewGroup事件分發(fā)
通過上面的分析,現(xiàn)在事件已經(jīng)從Activity->ViewGroup巨坊。那么我們就分析ViewGroup#dispatchTouchEvent()方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 講解二
*/
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
//清空mFirstTouchTarget
resetTouchState();
}
// Check for interception.
/**
*講解一
*/
//檢查是否攔截事件
//1.當事件為ACTION_DOWN時
//2. 當ViewGroup不攔截事件交給子元素處理 條件成立 即mFirstTouchTarget != null
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調用事件攔截方法
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;
}
......
// Check for cancelation.
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;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//非MotionEvent.ACTION_CANCEL并且沒有攔截事件
//進入if語句撬槽,對ViewGroup的子元素進行遍歷
/**
*講解三
*/
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
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);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//對子元素進行遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
//1.子元素是否在做動畫
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//2.事件左邊是否落在子元素區(qū)域內
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//接受點擊事件的View根據(jù)1.2條件判斷
//是否能夠接受點擊事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不符合要求 執(zhí)行下一個循環(huán)
continue;
}
......
//調用此方法進行事件分發(fā)處理 如果有子元素
//那么參數(shù)中的child!=null
/**
*講解四
*/
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
//條件滿足跳出循環(huán)
//這里是跳出內部for循環(huán)不是外部的
//其實就是break的用法
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/**
*講解五
*/
//對mFirstTouchTarget 進行賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
}
}
/**
*講解六
*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
......
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
/**
*講解七
*/
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
......
}
關于ViewGroup的事件分發(fā)源碼(即dispatchTouchEvent()方法)趾撵,還是比較長的侄柔,同時也是難點,下面我們將上面的代碼拆分來看,來具體分析占调。
part1
·
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 講解二
*/
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
//清空mFirstTouchTarget
resetTouchState();
}
// Check for interception.
/**
*講解一
*/
//檢查是否攔截事件
//1.當事件為ACTION_DOWN時
//2. 當ViewGroup不攔截事件交給子元素處理 條件成立 即mFirstTouchTarget != null
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調用事件攔截方法
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;
}
-
講解一
這個方法首先會判斷暂题,是否攔截當前事件。這個條件第一個好理解究珊。一個事件的系列一定是由DOWN開始的薪者,那么就會進入條件語句。調用onInterceptTouchEvent(ev)開始進行事件攔截剿涮。關于第二個代碼中有解釋言津。這個方法是在那里還原初始值與賦值請看下面。 -
講解二
事件開始時會調用 resetTouchState();清空mFirstTouchTarget
part2
//非MotionEvent.ACTION_CANCEL并且沒有攔截事件
//進入if語句取试,對ViewGroup的子元素進行遍歷
/**
*講解三
*/
if (!canceled && !intercepted) {
......省略
//對子元素進行遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
//1.子元素是否在做動畫
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//2.事件左邊是否落在子元素區(qū)域內
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//接受點擊事件的View根據(jù)1.2條件判斷
//是否能夠接受點擊事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不符合要求 執(zhí)行下一個循環(huán)
continue;
}
......
//調用此方法進行事件分發(fā)處理 如果有子元素
//那么參數(shù)中的child悬槽!=null
/**
*講解四
*/
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
//條件滿足跳出循環(huán)
//這里是跳出內部for循環(huán)不是外部的
//其實就是break的用法
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/**
*講解五
*/
//對mFirstTouchTarget 進行賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
}
}
-
講解三
進入if語句,判斷條件為沒有對事件進行攔截想括,同時事件沒有結束陷谱。對ViewGroup的子元素進行遍歷 -
講解?四
通過判斷,將ViewGroup的子元素進行遍歷瑟蜈,找到能夠處理點擊事件的子元素并調用dispatchTransformedTouchEvent()方法烟逊,進行事件的分發(fā)。 -
講解五
當子元素能夠處理點擊事件铺根,就調用addTouchTarget()方法宪躯,對mFirstTouchTarget()方法進行賦值。那么下次再進入講解一方法位迂。
part3
/**
*講解六
*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
......
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
/**
*講解七
*/
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
-
講解六
講解三中的if語句不成立表示對事件進行攔截访雪。那么直接走到了講解六,并且mFirstTouchTarget == null沒有在子元素的遍歷中賦值掂林,即條件成立臣缀。執(zhí)行dispatchTransformedTouchEvent()方法。 -
講解七
在同一事件系列結束后調用resetTouchState();對mFirstTouchTarget清空還原泻帮。
這樣我們就將ViewGroup#dispatchTouchEvent()方法分析完成了精置。
在上面的講解中我們多此提到mFirstTouchTarget與dispatchTransformedTouchEvent()方法。前者已經(jīng)說明了他的作用與賦值及清空還原的位置锣杂。對于后者脂倦,這個方法其實就是ViewGroup對事件分發(fā)番宁。看下他的源碼:
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) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
}
是ViewGroup#dispatchTransformedTouchEvent()方法赖阻,其他方法省略蝶押。可以可看到火欧,如果ViewGroup有子元素同時子元素可以處理點擊事件棋电。那么就會調用子元素的child.dispatchTouchEvent(event);方法。如果是child是ViewGroup繼續(xù)上面的循環(huán)苇侵,如果子元素是View离陶,那么就或調用View.dispatchTouchEvent(event)方法。關于這個方法我們后面分析衅檀。如果child為空(即講解六),那么就會調用super.dispatchTouchEvent(event)方法霎俩,那么就會調用ViewGroup父類的哀军,即View.dispatchTouchEvent(event)方法,ViewGroup自己處理點擊事件打却。最后都會默認(是默認不是一定) 調用onTounchEvent()方法杉适。
通關對源碼與這七個重要部分的講解。我們可以總結如下幾點:
- 一個事件序列只能一個View進行攔截且消耗柳击。由講解三我們知道猿推。如果攔截事件,就不會進入if語句對子元素進行遍歷與事件分發(fā)捌肴。同時又講解六我們知道蹬叭,如果攔截了某一事件(如MOVE)那么統(tǒng)一事件序列內的所有事件都交給它處理。
- 某個View一旦攔截事件状知,那么這一事件序列只能有它來處理(由1可知)弥雹。同時我們知道既然攔截就無法進入講解三中转捕,那么mFirstTouchTarget就無法被賦值,那么講解一中的條件就不成立。所以調用onInterceptTouchEvent()不會再被調用运悲。其實通過1.我們也都理解,如果攔截那么同一事件序列的所有事件都間給當前View處理察绷。你攔截就說明你必須全都處理炮叶。那我還問你干啥。
- 贷揽。dispatchTransformedTouchEvent()方法中棠笑,當dispatchTouchEvent()的返回值與dispatchTransformedTouchEvent()返回值相同,由講解六得知擒滑。這樣會直接影響ViewGroup#dispatchTouchEvent()返回值(兩者相同)腐晾。也就是說:View一旦開始處理事件叉弦,如果它不消耗ACTION_DOWN事件(onTounchEvent()返回false)。那么統(tǒng)一事件序列的其他事件都不會交給他處理藻糖。并會重新交給父元素(注意是父元素淹冰,不是父類)去處理,即父元素onTounchEvent()會被調用巨柒。
- onInterceptTouchEvent()默認返回false樱拴,即默認不攔截任何事件。
- View沒有onInterceptTouchEvent()洋满。一旦事件傳遞給他晶乔,那么他的onTouchEvent就會調用。
關于ViewGroup的源碼分析我們也就到這里了牺勾。有的啰嗦正罢。不過詳細才能跟好的理解與全面
3. View事件分發(fā)
由上面dispatchTransformedTouchEvent()方法可知,最后方法無論是ViewGroup消耗還是View消耗都會調用View#dispatchTouchEvent()方法驻民。那么我們就來看這個方法:
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED //是否是enable可點擊的 按鈕默認都是可點擊的 ImageView 不可點擊
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//調用onTouchEvent(event)方法 返回值直接影響此方法的返回值
//返回值與次方法相同
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
View的dispatchTouchEvent比較簡單翻具。首先判斷mOnTouchListener!=null同時li.mOnTouchListener.onTouch(this, event)返回為true那么result = true;回还。那么下面的 if (!result && onTouchEvent(event)) 中的第一個條件就不會成立所以onTouchEvent(event)永遠不會得到執(zhí)行裆泳。有此可見onTouch()優(yōu)先級要高于onTouchEvent(event)。
下面我們看下默認情況進入onTouchEvent(event)方法中:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
/**
*講解一
*/
//首先判斷當前View是不是DISABLED不可用狀態(tài)
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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.
//如果不可用 同時當前控件的clickable與long_clickable
//與CONTEXT_CLICKABLE全是false
//那么才返回false
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果View有代理會執(zhí)行這個方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/**
*講解二
*/
//只要控件的clickable與long_clickable
//與CONTEXT_CLICKABLE 有一個為true 就進入次循環(huán)
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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)) {
/**
*講解三
*/
//onClickListener監(jiān)聽在此方法中
performClick();
}
}
}
......
}
break;
......
}
//默認返回 true
/**
*講解四
*/
return true;
}
return false;
}
這部分代碼比較簡單柠硕。主要的都有注釋工禾。如果控件!=DISABLED蝗柔,那么就會進入同時講解二判斷有一個成立闻葵。就會進入switch語句。當接收到MotionEvent.ACTION_UP是癣丧。最后執(zhí)行performClick()方法.這個方法代碼如下:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
可以看到我們的設置setOnclickListener就是在這個賦值笙隙,li.mOnClickListener.onClick(this);就會調用我們的onClik方法。
那么有的同學會問View的longClickable默認是false坎缭,同時TextView的clickable也為false竟痰,那么為何我們給TextView設置setOnclickListener也能生效。我們下來看下TextView源碼其他默認clickable=false的控件是一樣的掏呼。:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
//設置clickable
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
//設置longclickable
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
可以看到在設置監(jiān)聽時坏快,方法內部已經(jīng)幫我們設置了。
下面我們在針對onTouchEvent(MotionEvent event)方法來拆分分析下:
part1
/**
*講解一
*/
//首先判斷當前View是不是DISABLED不可用狀態(tài)
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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.
//如果不可用 同時當前控件的clickable與long_clickable
//與CONTEXT_CLICKABLE全是false
//那么才返回false
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果View有代理會執(zhí)行這個方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
-
講解一
由代碼可知即使控件是DISABLED狀態(tài)憎夷,只要clickable與longclickable有一個返回true那么此方法就返回true莽鸿,即事件被消費。但是不會執(zhí)行onClick()方法。這點通過代碼很容易理解祥得。
part2
/**
*講解二
*/
//只要控件的clickable與long_clickable
//與CONTEXT_CLICKABLE 有一個為true 就進入次循環(huán)
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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)) {
/**
*講解三
*/
//onClickListener監(jiān)聽在此方法中
performClick();
}
}
}
......
}
break;
......
}
//默認返回 true
/**
*講解四
*/
return true;
}
-
講解二
講解二中只要有一個條件滿足兔沃。就會進入switch語句。當接收到MotionEvent.ACTION_UP時(前提MotionEvent.ACTION_DOWN也接收到了)會經(jīng)過判斷最后執(zhí)行 performClick();方法级及。 -
講解三
performClick()方法內部會執(zhí)行我們設置的監(jiān)聽乒疏,即onClick()方法。 -
講解四
由代碼可知只要講解二中的if語句成立饮焦,不管進入switch中的任何ACTION或是都不進入怕吴,返回值都是true,即事件消費了县踢。同時講解四也證明默認情況下是返回true
總結
下面我們用流程圖在來總結下:
圖片來源 侵權即刪
流程圖真的懶得畫了转绷。一篇文章學習得3.4天。寫出來又得很長時間硼啤,所以大家勿怪议经。同時這張圖結合文章理解起來簡直是so easy。
其實關于Android事件分發(fā)機制優(yōu)秀的文章由很多谴返。如果觀看一篇文章無法完全掌握爸业,就多看幾篇文章。然后自己總結亏镰,結合。反正最后能理解成自己的就算成功了拯爽。
結語
本人是個菜鳥索抓,如果文章哪里有錯誤,歡迎指出毯炮。有問題也可以留言逼肯。最后如果文章對您有幫助。感謝支持桃煎。
優(yōu)秀干貨
Android事件分發(fā)機制
Android事件分發(fā)機制詳解:史上最全面篮幢、最易懂
《Android開發(fā)藝術探索》