Android事件分發機制本質是樹的深度遍歷(圖+原始碼)
什麼是事件分發機制?
相關方法
事件分發機制相關的幾個方法:
View
- dispatchTouchEvent():處理事件。(注:不分發)
- onTouchEvent():觸發 onClick() 等點選事件的回撥。
onTouchEvent() 在基類 View 的 dispatchTouchEvent() 中被呼叫。onTouchEvent()在 clickable 或 longclickable 或 contextclickable 時預設返回true,即消耗事件。
ViewGroup
dispatchTouchEvent():分發事件 。
- 判斷當前事件是否需要呼叫 onInterceptTouchEvent() 對事件進行攔截。
- 若 onInterceptTouchEvent() 返回 true 則攔截,並處理事件。否則,則將事件分發給子View。
- 若沒有子View 或事件不被下層 View 消耗(即所有子View的 dispatchTouchEvent() 返回false)則呼叫基類 View 的dispatchTouchEvent() 對事件進行處理。
onInterceptTouchEvent():決定是否攔截事件(即是否分發給子View),返回true 表示攔截,否則不攔截。
onInterceptTouchEvent()(View沒有這個方法)在 ViewGroup 的dispatchTouchEvent() 中被呼叫。onInterceptTouchEvent()預設返回false,即不攔截。- onTouchEvent():繼承自 View,不重寫。
概念
概念:事件分發機制就是事件(MotionEvent)如何在view tree 中分發、處理的一種規則。
事件分發
概念:一個事件產生後,會先從Activity分發給Window,Window再分發給它裡面的頂級View(是view tree 的根)。頂級View接受到事件後會呼叫自身的dispatchTouchEvent(),在該方法中會迭代呼叫子View的 dispatchTouchEvent() 直到事件被攔截或消耗(若事件被攔截或消耗則結束分發/遍歷),這個過程實質就是對view tree的深度遍歷。事件分發只發生在 Activity 和 ViewGroup 中,View 不分發。 Activity 和 ViewGroup 對事件的分發都是通過呼叫 dispatchTouchEvent() 。
事件分發何時結束?
- 事件被攔截時。
- 事件被消耗時。
- 遍歷完整個 view tree 時。
事件攔截:即 onInterceptTouchEvent() 返回值是 true。只有ViewGroup才能攔截事件。
事件處理結果:即 dispatchTouchEvent() 返回值,true 表示消耗,false表示不消耗。
事件消耗:即 dispatchTouchEvent() 返回值是 true。事件處理
事件可以在 Activity 或 ViewGroup 或 View 中被處理,而且可被多次處理直到被消耗才停止處理。Activity:對事件的處理是通過呼叫自身的 onTouchEvent()。
View:View 接收到事件後會呼叫 dispatchTouchEvent() 直接對事件進行處理。在該方法裡會觸發onTouch() 或 onTouchEvent() 對事件進行處理。
ViewGroup:若沒有重寫dispatchTouchEvent(),都是通過呼叫基類 View 的 dispatchTouchEvent() 並將其返回值作為該 ViewGroup 的 dispatchTouchEvent() 的返回值。ViewGroup 何時對事件進行處理?
1、攔截事件或沒有子View 時。
2、下層的所有View 都未消耗事件時。View 何時對事件進行處理?
View 接受到事件後,(不會進行分發,因為沒有子View)會直接處理事件,並將事件處理結果返回給它的ViewGroup。
注:事件處理不等於事件消耗。
事件分發
事件在view tree中分發的流程(圖解)
“分發樹”:當觸碰事件產生時,ACTION_DOWN 產生時所觸控到的所有 View 按照父子關係可以組成一個 view tree 即“分發樹”,“分發樹”的根節點是頂級View,“分發樹”是整個Window 中完整的 view tree 的一部分,ACTION_DOWN 就是在這個“分發樹”中分發的。
事件在view tree中分發的流程:
ACTION_DOWN 事件
ACTION_DOWN 在具體應用程式app區域下分發的幾種常見情形:
不瞭解Android 手機介面組成參考:Android手機介面組成
總之,ACTION_DOWN 會深度遍歷“分發樹”並確定“消耗樹”。注:這裡的“消耗樹”指的就是上圖中的“消耗路徑”。後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)
後續同一序列事件都是沿著這一“消耗樹”分發(深度遍歷,但通常都是線性結構)的,且可被中途攔截但“消耗樹”不變。
若ACTION_DOWN找不到“消耗路徑”(即不被任何View消耗)也不被Activity消耗,那麼後續事件就會消失,不會被處理。
“消耗路徑”:從頂級View到消耗事件的View的最短路徑就是“消耗路徑”(線性結構的),該“消耗路徑”會與已有的“消耗樹”合併(若存在“消耗樹”的話。通常情況下是沒有的,因為ACTION_DOWN時會清空)。
“消耗樹”:一條或多條“消耗路徑”合併形成的。“消耗路徑”也是“消耗樹”的一種,只不過它只有一條“消耗路徑”。
同一個事件序列:指從手指接觸螢幕(觸發ACTION_DOWN事件)到手指離開螢幕(觸發ACTION_UP)以及期間產生的一系列事件。
注意:同一事件序列只有一條“消耗路徑”,因此,覺得混亂的話可以直接把“消耗樹”當做“消耗路徑”。
下面是將“分發樹”簡化為線性結構後事件分發的幾種情形:
紅色:代表ACTION_DOWN事件的分發路徑,不是“消耗路徑”。
藍色:代表ACTION_MOVE 和 ACTION_UP 事件分發路徑,也是“消耗路徑”。
1、我們重寫ViewGroup1 的dispatchTouchEvent 方法,直接返回true消費這次事件
ACTION_DOWN 事件從(Activity的dispatchTouchEvent)——–> (ViewGroup1 的dispatchTouchEvent) 後結束傳遞,事件被消費(如下圖紅色的箭頭程式碼ACTION_DOWN 事件的流向)。
2、我們在View 的onTouchEvent 返回true消費這次事件
3、我們在ViewGroup 2 的onTouchEvent 返回true消費這次事件
4、我們在Activity 的onTouchEvent 返回true消費這次事件
5、我們在View的dispatchTouchEvent 返回false(即重寫並直接返回false)並且Activity 的onTouchEvent 返回true消費這次事件
上面例子來源:圖解 Android 事件分發機制
注:下面的原始碼都來自Android-23版本。
事件在各結點中分發的流程(圖+原始碼)
Activity
流程圖
Activity的 dispatchTouchEvent() 的流程:
原始碼
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity對事件的分發是通過Window來分發的,並不攔截。
注意:Window只負責幫Activity分發事件,不處理,也就是沒有onTouchEvent方法,可以把它的分發功能當成Activity的一部分,然後忽略它。
ViewGroup
View和ViewGroup中的方法:
View:dispatchTouchEvent();onTouchEvent()
ViewGroup:重寫dispatchTouchEvent();onInterceptTouchEvent();
注意:ViewGroup及其子類並未重寫onTouchEvent()。流程圖
ViewGroup的dispatchTouchEvent方法的流程:
TouchTarget連結串列:每個ViewGroup都持有一個mFirstTouchTarget變數,該變數指向一個TouchTarget連結串列(不帶頭結點)的首結點,該連結串列的結點用於存放當前Viewgroup的子結點,該子結點必須是“消耗樹”上的結點。TouchTarget連結串列會在ACTION_DOWN被清空即“消耗樹”消亡。總之,ViewGroup 的 TouchTarget連結串列存放該 ViewGroup 在“消耗樹”上的所有子結點,mFirstTouchTarget指向它的首結點。
事件在 ViewGroup 中分發的流程(即 ViewGroup 的 dispatchTouchEvent() 的流程):
若為 ACTION_DOWN 事件:
是否攔截
當前 ViewGroup 結點若攔截事件則結束分發,否則會將事件分發給它的子View。只分發給“分發樹”上的子結點
遍歷 Window 中完整的 view tree,若當前結點不在觸碰區域內則進入下一結點,不對其分發事件。其實就是隻在“分發樹”上分發。
注:在事件是 pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等)時會形成一個“分發樹”。更新 TouchTarget 列表
在遍歷它在“分發樹”上的子結點時,若遍歷到的子結點在當前 ViewGroup 的 TouchTarget 連結串列中則將事件分發給該子結點(即呼叫該子結點的dispatchTouchEvent())並結束對當前 ViewGroup 的子結點的遍歷,否則,遍歷每個子結點,呼叫每個子結點的 dispatchTouchEvent()直到被消耗即返回 true 才結束遍歷並將該子結點插入到 TouchTarget 連結串列的頭部(其實就是合併到已有的“消耗樹”上,如果有“消耗樹”的話)。總之,遍歷子結點時,若子結點在已有的“消耗樹”上則分發給它並結束更新,否則呼叫子結點的 dispatchTouchEvent(),若返回true則將該子結點插入連結串列頭部結束更新。分發。
遍歷 TouchTarget列表,呼叫每個元素(即該 ViewGroup 在“分發樹”上的每個子View)的 dispatchTouchEvent() 。
若是後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP):
是否攔截
“消耗樹”上的 ViewGroup 結點若攔截事件則結束分發,否則會將事件分發給它的子View。遍歷TouchTarget列表,呼叫每個元素(即該 ViewGroup 在“消耗樹”上的每個子View)的 dispatchTouchEvent() 。
是否攔截:當 ViewGroup 的 disallowIntercept 為 false 也就是允許攔截時,若事件為ACTION_DOWN 或mFirstTouchTarget != null 必定觸發當前ViewGroup的onInterceptTouchEvent()。滑動衝突的處理就是在 ACTION_MOVE 事件在“消耗樹”上分發時在中途將它攔截,參考:Android 滑動衝突的處理。
原始碼
public boolean dispatchTouchEvent(MotionEvent ev) {
//關鍵步驟:
//1、是否攔截事件。
//2、更新TouchTarget 列表。
//3、分發。
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
......
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
//每次事件序列開始(即發生ACTION_DOWN事件時)都會
//重置mFirstTouchTarget、mGroupFlags的值為初始值(分別為null 和 允許攔截)。
resetTouchState();
}
//1、是否攔截事件
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//mFirstTouchTarget指向一個TouchTarget列表的首結點。
//TouchTarget列表是Linked List,若事件分發給某個子View後可被“下層”消耗,
//則新增該子View到列表頭部(採用的是頭插法)。
//“下層”包括當前ViewGroup裡面的所有在當前事件觸控範圍內的View,不僅僅指它的子View。
//簡單的說就是當前事件之前的事件被下層消耗則不為null。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//disallowIntercept是否不被允許攔截事件。
//可通過子元素呼叫requestDisallowInterceptTouchEvent(boolean b)來
//設定mGroupFlags的值,從而改變disallowIntercept。
//但是這一條件判斷對ACTION_DOWN無效(總為true),因為會重置mGroupFlags(見上面程式碼)。
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;
}
//下面的程式碼都是講事件是如何往下層分發的,分兩步:2、3
//2、更新TouchTarget 列表,事件為pointer down時才需要。
//pointer down包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等,參考下面判斷條件。
//注意:新增的元素必須是事件範圍內也就是觸控到的區域內的子View,同時,沿著這個子View往下分發能消耗事件,
//或者說,呼叫該子View的dispatchTouchEvent方法返回結果是true。
// 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;
//非canceld && 不攔截 時才向下層分發事件
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//注意:以下幾種情況才需要更新TouchTarget 列表
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//遍歷子元素
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//若該View不被觸碰到或不能相應事件則剔除,其實就是隻允許對“分發樹”遍歷
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {//子元素已經在TouchTarget連結串列中,結束遍歷。
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//dispatchTransformedTouchEvent()會呼叫子元素的dispatchTouchEvent()。
// Child wants to receive touch within its bounds.
......
//採用頭插法將子元素新增到TouchTarget列表中
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//已找到消耗此事件的路徑且已分發給下層的目標View
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//並未找到消耗事件的路徑 && mFirstTouchTarget 不為空時,將TouchTarget連結串列中
//最近最少被新增的target賦給它,即將連結串列的最後一個結點的引用賦給newTouchTarget。
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;
}
}
}
//3、分發:往下層分發事件。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//當做普通View對待,而不是ViewGroup。
//會呼叫super.dispatchTouchEvent()方法,最終呼叫自身的onTouchEvent方法.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//在上面更新TouchTaget列表時已分發完畢。
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;
}
關鍵步驟:
1、是否攔截事件。
- 當ViewGroup的disallowIntercept為false也就是允許攔截時,若事件為ACTION_DOWN 或mFirstTouchTarget != null 一定會觸發當前ViewGroup的onInterceptTouchEvent()。
- 子元素能夠通過呼叫requestDisallowIntercept(boolean b)來控制父容器能否呼叫onInterceptTouchEvent()。
2、更新TouchTarget 列表。
- 本質是尋找該事件的“消耗路徑”的下一個結點。若TouchTarget列表中有元素處於當前事件的觸控區域內,則結束更新,否則會遍歷呼叫觸控區域內的子View的dispatchTouchEvent方法,直到返回true並新增到列表。。
- 事件屬於pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等),則要更新列表。
- 更新列表規則:新增的元素必須是事件範圍內也就是觸控到的區域內的子View,同時,沿著這個子View往下分發能消耗事件,或者說,呼叫該子View的dispatchTouchEvent方法返回結果是true。
- 更新列表的步驟:遍歷判斷 1、子View是在觸控區域內,不是則continue,進入下一個迴圈。2、子View是否已在列表中,是則break,結束遍歷。3、呼叫子View的dispatchTouchEvent方法,返回結果是true則用頭插法加入列表(在這一步中已進行分發,後面的“分發”程式碼不會再分發一次)。
3、 分發。
- 遍歷TouchTarget列表,呼叫每個元素的dispatchTouchEvent方法。
總結:ACTION_DOWN若能被消耗則會確定“消耗樹”,後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿著這一“消耗樹”分發的,且可被中途攔截。
ViewGroup的分發過程可用如下虛擬碼表示:
dispatchTouchEvent(MotionEvent ev){
boolean consume = false;//事件是否被消耗,若被消耗則該事件的分發結束
if (onInterceptTouchEvent(ev)) {//攔截事件
consume = super.dispatchTouchEvent(ev);//即View.dispatchTouchEvent(ev)
}else {
//遍歷子元素,將事件分發給子元素,直到事件被消耗。
//其實,實際程式碼只需遍歷TouchTarget列表中的元素,不需要遍歷所有子View。
View child = null;
for (int index = 0; index < childNum; index++) {
child = getChild(index);
if (null != child) consume = child.dispatchTouchEvent(ev);
if (consume) break;
}
//遍歷結束但事件沒有被消耗,對事件進行處理。
if (!consume) consume = super.dispatchTouchEvent(ev);
}
return consume;
}
View
View 的 dispatchTouchEvent() 直接處理事件,不分發。
事件處理
Activity
Activity 對事件的處理都是通過呼叫自身的 onTouchEvent() 。
原始碼:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
View
無論View 還是ViewGroup,若沒有重寫dispatchTouchEvent(),對事件的處理都是通過呼叫基類View的 dispatchTouchEvent(),最終呼叫 onTouch() 或 onTouchEvent() 。
基類 View 的 dispatchTouchEvent() 流程:
若View設定了 OnTouchListener 且 onTouch() 返回 true 則dispatchTouchEvent() 返回 true,不會呼叫onTouchEvent()。否則,會呼叫 onTouchEvent() 且 onTouchEvent() 的返回值就是dispatchTouchEvent() 的返回值。
在View的onTouchEvent()中若View是clickable或longclickable的則會呼叫onClick()(若有設定OnClickListener)。clickable或longclickable或contextclickable時預設返回true,否則返回false。
流程圖
View的dispatchTouchEvent方法的流程:
原始碼
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;//是否消耗事件
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//若設定了OnTouchListener,則先呼叫onTouch()。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//若onTouch()沒有消耗事件則呼叫onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
若View是enabled的,且設定了OnTouchListener,則先呼叫onTouch(),若onTouch()返回true則分發結束,否則,接著呼叫onTouchEvent()。
ViewGroup
ViewGroup對事件處理是通過呼叫基類View的dispatchTouchEvent() ,最終呼叫 onTouch() 或 onTouchEvent() 。
ViewGroup的onTouchEvent()繼承自View,本身不重寫。ViewGroup的實現類也不重寫該方法。
總結
事件分發機制概要流程:
上面圖片來源:Android事件分發機制 詳解攻略,您值得擁有
事件分發機制詳細流程:
總之,ACTION_DOWN 會深度遍歷“分發樹”並確定“消耗樹”,後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿著這一“消耗樹”分發(深度遍歷,但通常都是線性結構)的,且可被中途攔截但“消耗樹”不變。
相關文章
- Android事件分發:從原始碼角度分析View事件分發機制Android事件原始碼View
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- Android從原始碼角度剖析View事件分發機制Android原始碼View事件
- Android 事件分發機制原始碼詳解-最新 APIAndroid事件原始碼API
- 基於原始碼分析 Android View 事件分發機制原始碼AndroidView事件
- Android事件分發機制Android事件
- 事件分發機制(二):原始碼篇事件原始碼
- Android 事件分發機制的理解Android事件
- Android的MotionEvent事件分發機制Android事件
- vscode原始碼分析【五】事件分發機制VSCode原始碼事件
- Android View 的事件體系 -- 事件分發機制AndroidView事件
- android事件分發機制詳解Android事件
- Android事件分發機制三:事件分發工作流程Android事件
- 淺談Android中的事件分發機制Android事件
- 【Android基礎】講講Android的事件分發機制Android事件
- 淺談Android 事件分發機制(二)Android事件
- Android事件分發機制簡單理解Android事件
- 從原始碼看 Android 事件分發原始碼Android事件
- Android事件分發原始碼歸納Android事件原始碼
- Android View 事件分發原始碼分析AndroidView事件原始碼
- 面試:講講 Android 的事件分發機制面試Android事件
- Android系統原始碼剖析-事件分發Android原始碼事件
- Android事件分發機制,你瞭解過嗎?Android事件
- android觸控事件分發機制,曾困惑你我的地方Android事件
- 「Android」分析EventBus原始碼擴充套件Weex事件機制Android原始碼套件事件
- 二叉樹的遍歷演算法【和森林的遍歷】【PHP 原始碼測試】二叉樹演算法PHP原始碼
- Android自定義View之事件分發機制總結AndroidView事件
- Android原始碼角度分析事件分發消費(徹底整明白Android事件)Android原始碼事件
- 二叉樹的深度、寬度遍歷及平衡樹二叉樹
- React原始碼分析 – 事件機制React原始碼事件
- Android事件分發機制:基礎篇:最全面、最易懂Android事件
- Android事件分發機制五:面試官你坐啊Android事件面試
- View事件分發機制分析View事件
- cocos EventDispatcher事件分發機制事件
- Flutter 命令本質之 Flutter tools 機制原始碼深入分析Flutter原始碼
- 樹的遍歷方式
- Vue.js原始碼——事件機制Vue.js原始碼事件
- 二分搜尋樹系列之[ 深度優先-層序遍歷 (ergodic) ]Go