Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。 ...
目錄
- 一、目的
- 二、環境
- 三、相關概念
- 四、詳細設計
- 五、小結&問題點
- 六、代碼倉庫地址
- 七、參考資料
一、目的
最開始接觸Android時,僅僅是知道Android系統存在的點擊事件、觸摸事件,但是並不清楚這些事件的由來。
之後,在面試Oppo和美圖時,皆有問到Android的事件分發機制,但是都被問得很懵逼,歸根到底都是對於其實現邏輯的不理解。
隨後,想去彌補該模塊的不足,瀏覽很多關於Android事件分發的博文,但仍存在一些疑惑,就想著去閱讀下源碼,整理下筆記,希望對同學們有幫助。
二、環境
- 版本:Android 11
- 平臺:展銳 SPRD8541E
三、相關概念
3.1 事件分發
Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。
四、詳細設計
4.1應用佈局
4.1.1 應用佈局結構
如下為一個Activity打開後,其對應視圖的層級結構。
4.1.2 LayoutInspector
Layout Inspector是google提供給我們進行佈局分析的一個工具,也是目前google在棄用Hierarchy View後推薦使用的一款佈局分析工具。
4.2 關鍵View&方法
4.2.1 相關View
組件 | 描述 |
---|---|
Activity | Android事件分發的起始端,其為一個window視窗,內部持有Decorder視圖,該視圖為當前窗體的根節點,同時,它也是一個ViewGroup容器。 |
ViewGroup | Android中ViewGroup是一個佈局容器,可以嵌套多個 ViewGroup 和 View,事件傳遞和攔截都由 ViewGroup 完成。 |
View | 事件傳遞的最末端,要麼消費事件,要麼不消費把事件傳遞給父容器 |
4.2.2 相關方法
方法 | 描述 |
---|---|
dispatchTouchEvent | 分發事件 |
onInterceptTouchEvent | 攔截事件 |
onTouchEvent | 觸摸事件 |
4.2.3 View與方法關係
組件 | dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
---|---|---|---|
Activity | ✔ | ❌ | ✔ |
ViewGroup | ✔ | ✔ | ✔ |
View | ✔ | ❌ | ✔ |
4.3 事件分發概念圖
4.3.1 事件分發類圖
4.3.2 事件分發模型圖
Android的ACTION_DOWN事件分發如圖,從1-9步驟,描述一個down事件的分發過程,如果大家能懂,就不用看下麵文字描述了(寫完這個篇幅,感覺文字好多,不好理解!)
- ACTION_DOWN事件觸發。 當我們手指觸摸屏幕,tp驅動會響應中斷,通過ims輸入系統,將down事件的相關信息發送到當前的視窗,即當前的Activity。
- Activity事件分發。 會引用dispatchTouchEvent()方法,對down事件分發。Activity本身會持有一個window對象,window對象的實現類PhoneWindow會持有一個DecorView對象,DecorView是一個ViewGroup對象,即我們可以理解為,Activity最終會將事件分發給下一個節點——ViewGroup。
- ViewGroup事件攔截。 ViewGroup接收到事件後,會先引用onInterceptTouchEvent(),查看當前的視圖容器是否做事件攔截。
- ViewGroup消費事件。 如當前的ViewGroup對事件進行攔截,即會調用onTouchEvent(),對事件消費。
- ViewGroup事件不攔截。 則ViewGroup會繼續遍歷自身的子節點,並且當事件的坐標位於子節點上,則繼續下發到下一個節點。ViewGroup的子節點有可能是View,也可能是ViewGroup(當然,ViewGroup最後也是繼承於View的,突然感覺有點廢話)。
- ViewGroup事件分發。 目標視圖如果是ViewGroup,會引用其super類的dispatchTouchEvent()方法,即事件下發,不管目標視圖是View或者ViewGroup最終引用的是View類的分發方法。
- View事件消費。 在View的dispatchTouchEvent()方法中會根據當前View是否可以點擊、onTouch()是否消費、onTouchEvent()是否消費等條件,來判斷當前是否為目標View。
- View事件未消費。 View事件未消費,則其父節點,即ViewGroup會調用onTouchEvent()方法,並根據返回值來決定是否消費事件。
- ViewGroup事件未消費。 ViewGroup事件未消費,擇其父節點,即Actviity會調用onTouchEvent()方法
PS:
(1) ACTION_MOVE和ACTION_UP事件,流程與ACTION_DOWN的分發過程基本一致,MOVE和UP事件也是通過Activity開始,藉助DOWN事件產生的目標View,逐級分發。
(2) ACTION_CANCEL事件,是在down與up、move事件切換過程中,事件被攔截,兩次的touchTarget目標view不一致,而產生的事件。用於對之前的目標View做恢復處理,避免down與up/move事件不對稱。
4.4 Activity組件
4.4.1 Activity->dispatchTouchEvent()
底層上報的事件信息,最終會引用到該方法。Activity會持有一個根視圖DecordView,事件最終會往該ViewGroup分發,如所有的View都未消費該事件,則最終由Activity的onTouchEvent()
來兜底處理。
@frameworks\base\core\java\android\app\Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {//Step 1. 查看Window對應的View是否分發該事件
return true;
}
return onTouchEvent(ev);//Step 2. 如果沒有組件消費事件,則由Activity兜底處理
}
4.4.2 Activity->getWindow()
我們每次啟動一個Activity的組件,會先打開一個window視窗,而PhoneWindow是Window唯一的實現類。
@frameworks\base\core\java\android\app\Activity.java
public Window getWindow() {
return mWindow;
}
final void attach(Context context, ActivityThread aThread...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);//PhoneWindow是Window視窗唯一的實現類
...
}
PhoneWindow對象內部持有DecorView對象,而該View正是該視窗對應的視圖容器,也是根節點。(此部分不具體分析)
@frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder. Callback {
...
private DecorView mDecor;//
...
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);//往View的根節點分發事件
}
}
4.4.3 Activity->onTouchEvent()
Activity的onTouchEvent方法,是在沒有任何組件消費事件的情況下,觸發的方法。
@frameworks\base\core\java\android\app\Activity.java
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
4.5 ViewGroup組件
ViewGroup組件在整個事件分發的模型中,既有分發事件的責任,又要具備處理事件的能力,真的典型的當爹又當媽。
當Activity調用superDispatchTouchEvent,即最終會使用到DecorView的superDispatchTouchEvent方法,而DecorView是繼承於ViewGroup,即最終會引用ViewGroup的dispatchTouchEvent方法。
4.5.1 ViewGroup->dispatchTouchEvent()
此方法為事件分發最核心的代碼。其主要處理如下四件事情:
Setp 1. 重置事件。 一次完整觸摸的事件:DOWN -> MOVE -> UP,即我們可以理解為DOWN是所有觸摸事件的起始事件。當輸入事件是ACTION_DOWN時,重置觸摸事件狀態信息,避免產生干擾。
Step 2. 攔截事件。 攔截事件是ViewGroup特有的方法,用於攔截事件,並將該事件分發給自己消費,防止事件繼續下發。
Step 3.查找目標View。 查找目標View主要針對於Down事件。當ViewGroup未攔截事件,且輸入事件是ACTION_DOWN時,會遍歷該ViewGroup的所有子節點,並根據觸摸位置的坐標,來決定當前子節點是否是下一級目標View。當找到目標View節點後,會分發Down事件,並記錄該節點信息。
Step 4.下發事件。 如果目標View未找到的話,則會將事件交由自己的onTouchEvent()處理;如果目標View已經找到,則Down事件就此結束(此處暫不考慮多指場景);Move和Up事件將繼續下發(預設情況下Move、Up和Down事件是成對出現的,如果目標View已經存在,則Down事件已經下發,即意味著Move和Up事件也需要下發給對應的目標View)。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {//Step 1.重置事件信息,避免影響下一次事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//Step 2.攔截事件
ev.setAction(action); // restore action in case it was changed
}
}
...
if (!canceled && !intercepted) {//Step 3.查找目標View
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) {//遍歷所有的子節點
...
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {// 子節點不可以接收事件,或者觸摸位置不在子節點的範圍上
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//找到目標View
...
break;
}
}
...
}
...
}
}
//Step 4.根據找到的目標View情況,繼續下發事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//沒有找到目標View或者事件被攔截,事件下發給自己
} else {
...
while (target != null) {//多組數據,一般是指多指場景
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//此場景一般是down事件
handled = true
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {//此場景一般是move、up事件
handled = true;
}
...
}
predecessor = target;
target = next;
}
...
}
...
return handled;
}
4.5.2 ViewGroup->dispatchTransformedTouchEvent()
事件分發關鍵方法,主要用於向目標View分發事件,具體邏輯如下:
Step 1.Cancel事件分發。 之前我們提過Down和Up事件是成對存在的,如果Down事件已經下發的情況下,Up事件卻因為事件攔截等原因,未能下發給目標View,目標View未收到Up事件,此時就可能產生一些按壓狀態的異常問題,故,在當前場景下,將會分發一個ACTION_CANCEL事件給目標View。
Step 2.事件處理。 如果事件未找到目標View,則child會為null,此時的事件將由自身處理。
Step 3.事件分發。 如果事件還存在目標View,則此時的事件會再分發。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//Step 1.下發取消事件
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
if (child == null) {//Step 2.如果事件未找到目標View,則觸摸事件會發給自己
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);//Step 3.找到目標View,事件下發給子節點
}
...
return handled;
}
4.6 View組件
View組件在事件處理模型中,主要是處理事件。我們知道ViewGroup,也是繼承於View,所以ViewGroup也是同樣具備View的處理事件能力。
4.6.1 View->dispatchTouchEvent()
Step 1.觸發onTouch()方法。 如果當前的View是可點擊的,且配置了onTouch事件監聽,則觸發該View的onTouch()方法。
Step 2.觸發onTouchEvent()方法。 如果該事件在上一步的onTouch()函數中未被消費,則觸發onTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//Step 1.觸發onTouch事件
result = true;
}
if (!result && onTouchEvent(event)) {//Step 2.如onTouch未消費,觸發onTouchEvent事件
result = true;
}
}
...
return result;
}
4.6.2 OnTouchListener->onTouch()
View可以設置事件監聽,用於監聽onTouch事件的回調,當然,像我們常見的onClick()、onLongClick()等事件也可監聽,其相關源碼如下:
@frameworks\base\core\java\android\view\View.java
public void setOnTouchListener(OnTouchListener l) {//設置onTouch監聽
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
public interface OnTouchListener {//Touch介面,用於回調onTouch事件
boolean onTouch(View v, MotionEvent event);
}
4.6.3 View->onTouchEvent()
事件如未被onTouch消費掉,則會引用到onTouchEvent()方法,該方法會涉及ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE事件的處理,View的onClick()、onLongClick()也是由該方法觸發。此外,如果當前的View是可點擊的話,則直接消費該事件。
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//當前View是否可點擊
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP://抬起
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
if (!focusTaken) {
removeLongPressCallback();//若有長按事件未處理,則移除長按事件
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {//通過Hanlder將點擊事件發送到主線程執行
performClickInternal();//如果不成功,則直接引用點擊事件
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();//更新按鈕的按壓事件
}
...
break;
case MotionEvent.ACTION_DOWN://按下
...
if (isInScrollingContainer) {//在可滾動的容器內,為了容錯,延遲點擊
...
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);//設置按下的狀態
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);//開啟一個長按延時事件
}
break;
case MotionEvent.ACTION_CANCEL://取消
...
break;
case MotionEvent.ACTION_MOVE://移動
...
break;
}
return true;//如果是可點擊的View,即消費事件
}
...
return false;
}
4.7 例子-點擊事件時序圖
如下是Android的點擊事件時序圖,如果能夠理解單擊事件的由來,對於整個事件分發的知識要點已大體掌握。
五、小結&問題點
- 事件分發流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的處理過程;
- ACTION_CANCEL事件的使用場景?父控制項對move事件攔截場景?
- 單擊、長按、觸摸事件的產生過程?
- 點擊一個View未抬起,同時move該事件直至離開當前View的範圍,處理過程如何?
- 如果所有View都未消費事件,流程如何?
- ViewPage+ListView,左右滑動和上下滑動衝突的解決問題?即事件攔截過程?
- 普通的View是根據什麼來決定是否消費事件,例如Button?
=>答:如無重寫onTouchEvent事件,根據當前的View是否可點擊,來決定是否消費事件。
我最開始沒有看源碼,直接去看博客上的內容,彎彎繞繞,似懂非懂。在面試的過程中,面試官舉個場景分析流程,我都懵逼,分析不出來,現場很尷尬。之後看源碼,整體流程代碼量很少,感嘆於Android事件分發流程的設計,很少的代碼量,卻承載了很重要的功能,而沒有見過該模塊發生過異常。
多讀書,多看報,少吃零食,多睡覺!
六、代碼倉庫地址
Demo地址: https://gitee.com/linzhiqin/custom-demo
七、參考資料
https://zhuanlan.zhihu.com/p/623664769?utm_id=0
事件分發視頻(總結很好,但是得先理解基本概念,才方便學習)
https://www.bilibili.com/video/BV1sy4y1W7az?p=1&vd_source=f222e3bf3083cad8d9f660629bc47c16