二、View 事件分發機制
本文是自己看過一些資料後的總結,如要詳細瞭解事件分發機制,請看「參考」 內的文章。
一、事件分發基礎認知
1.1 當我們在談論事件分發時,到底再談論什麼?
-
當用戶觸控式螢幕幕時,會產生點選事件 (Touch 事件)
而 Touch 事件的相關細節(發生觸控的位置、時間等)都被封裝成了MotionEvent 物件
所以當我們討論事件分發時,實際是在討論,是誰來處理這個 MotionEvent 物件,這個 MotionEvent 會不斷傳遞。
1.2 那麼 MotionEvent 會在哪些物件之間傳遞呢?
-
Activity、ViewGroup、View
牢記:Android 事件分發的流程是Activity -> ViewGroup -> View
即:1個點選事件發生後,事件會先傳遞到 Activity, 再傳到 ViewGroup,最終再傳到 View。
所以當我們分析時,會首先從 Activity 開始分析
1.3 事件分發過程中都會經歷哪些方法來進行 MotionEvent 的傳遞?
- dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()
- dispatchTouchEvent:用於分發點選事件,當點選事件傳遞給當前 View 時,就會呼叫該方法。
- onInterceptTouchEvent:攔截某個事件,只有 ViewGroup 存在這個方法。在 ViewGroup 的 dispatchTouchEvent 內部呼叫。
- onTouchEvent:用於處理點選事件,在 dispatchTouchEvent 內部呼叫。
二、事件分發機制
2.1 Activity 事件分發機制
- 當一個點選事件開始時,會進行如下傳遞 Activity.dispatchTouchEvent() -> PhoneWindow.superDispatchTouchEvent() -> DecorView.superDispatchTouchEvent() -> ViewGroup.dispatchTouchEvent()
- 我們可以看到在這個過程中,事件傳到了 ViewGroup,到此 Activity 的分發過程基本結束了。其實在 DecorView 呼叫的時候,我們就可以結束 Activity 的傳遞了,根據原始碼,此時 DecorView 的 superDispatchTouchEvent 會呼叫 ViewGroup 的 dispatchTouchEvent。
- 當 DecorView 的 superDispatchTouchEvent 返回true 的話,則說明有子 View 消費了事件,則 Activity.dispatchTouchEvent 會返回 true,事件分發結束。如果返回 false 的話,會呼叫 Activity 的 onTouchEvent 方法來消費事件,這個時候無論onTouchEvent 返回什麼,事件分發都結束了。
2.2 ViewGroup 事件分發機制
- ViewGroup 每次事件分發,都會呼叫 onInterceptTouchEvent 詢問是否攔截事件。
- 如果 ViewGroup.onInterceptTouchEvent 返回 true 代表攔截此事件。返回 true 的情況有兩種(一、自己手動重寫返回 true。 二、無 View接收事件,即點選空白處時)。
- 當 ViewGroup 攔截事件時,會呼叫 ViewGroup 父類的 dispatchTouchEvent 即 View.dispatchTouchEvent。 然後會自己處理該事件,呼叫自身的 onTouch -> onTouchEvent ->performClick -> onClick ,這是 View 的呼叫過程,具體看 View 的事件分發。
- 當 ViewGroup 不攔截事件時,會迴圈子View,找到被點選的相應子 View 控制元件,然後呼叫子 View 控制元件的 dispatchTouchEvent,這個時候也就實現了事件從 ViewGroup 到 View 的傳遞。
2.2.1 ViewGroup 怎麼判斷哪個子 View 被點選了
迴圈子 View 中,有這麼一段程式碼,用來判斷當前 View 是否被點選了
//child可接受觸控事件:是指child是可見的(VISIBLE);或者雖然不可見,但是位於動畫狀態。 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
這裡面有兩個方法用來判斷,canViewReceivePointerEvents
和isTransformedTouchPointInView
。
其中canViewReceivePointerEvents
判斷當前 View 是否能接收到 pointer events ,如果不能接收到,那就直接 continue 迴圈下一個了。
如果上面判斷是可以接受觸控事件的,那麼就會去判斷觸控座標(x,y)是否在 child 的可視範圍之內。
接下來具體看看isTransformedTouchPointInView
protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { // 首先 new 一個 float 陣列,用來存放點選的 x、y 座標 final float[] point = getTempPoint(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); //這是用來判斷點選是否在 View 內的具體方法。 final boolean isInView = child.pointInView(point[0], point[1]); if (isInView && outLocalPoint != null) { outLocalPoint.set(point[0], point[1]); } return isInView; }
final boolean pointInView(float localX, float localY) { return localX >= 0 && localX < (mRight - mLeft) && localY >= 0 && localY < (mBottom - mTop); }
通過這個方法可以看到,View 是怎樣判斷的。
- localX、localY : 是通過 ev.getX 和ev.getY 拿到的,在 View 基礎篇有講過,這兩個方法拿到的是相對當前 View 的座標。
- mRight、mLeft、mBottom、mTop :View 的四個頂點,具體可複習ofollow,noindex">View 基礎篇 。
2.3 View 事件分發機制
- 當事件傳遞到 View 時,會呼叫 View.dispatchTouchEvent,在這個裡面會首先判斷 View.onTouch() 所返回的值。注意是 onTouch 方法,並不是我們三大主要方法 onTouchEvent。
- View.onTouch 返回 true, 則事件被消費,不會再往下傳遞,即會呼叫如下程式碼塊,不會呼叫 onClick 了。
btn.setOnTouchListener(new OnTouchListener) { @Override public boolean onTouch(View v, MotionEvent evnet) { // 若在onTouch()返回true,從而使得View.dispatchTouchEvent()直接返回true,事件分發結束 // 若在onTouch()返回false,從而使得View.dispatchTouchEvent()中跳出If,執行onTouchEvent(event) return true; } }
- View.onTouch 返回 false,則會去呼叫自身的 onTouchEvent(),這個方法裡會具體判斷當前是什麼事件,從而做出相應操作,比如當前是 MotionEvent.ACTION_UP 時會呼叫 performClick(),這個方法裡就會消費我們經常寫的 setOnClickListener 裡的 onClick 方法了,然後返回 true。
- View 的 onTouchEvent 是事件傳遞的最後一個地方了,如果該 View 是可點選的,則一定會返回 true,此時事件分發結束。如果不可點選,會返回 false,此時事件就會回傳到 ViewGroup 的dispatchTouchEvent,然後會自己處理該事件,呼叫自身的 onTouch -> onTouchEvent ->performClick -> onClick 。 如果 ViewGroup 也返回 false,則會回傳到 Activity 的dispatchTouchEvent,去執行 Activity 的 onTouchEvent 方法來消費事件。如此就完成了事件的分發傳遞。
- 最後根據上述分析,可得知 OnTouchListener 優先於 onClickListener,即 onTouch() 的執行優先於 onClick()。
- 最後的最後:若1個控制元件不可點選(即非enable),那麼給它註冊onTouch事件將永遠得不到執行,具體原因看如下程式碼
// && 為短路與,即如果前面條件為 false,將不再往下執行 // 所以 onTouch() 能夠執行需2個前提條件: //1. mOnTouchListener 的值不能為空 //2. 當前控制元件必須是 enable 的。 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { result = true; } // 對於該類 非 enable 控制元件,若需監聽它的 touch 事件,就必須通過在該控制元件中重寫 onTouchEvent()來實現