寫在前面
轉眼間 面試系列 已經到了第九期了,由於文章將會持續更新,導致標題難看性,所以以後的標題將更正為本文類似的格式。
好了,話不多說,還是直入主題吧。
面試場景
講講 Android 的事件分發機制?
基本會遵從 Activity => ViewGroup => View 的順序進行事件分發,然後通過呼叫 onTouchEvent()
方法進行事件的處理。我們在專案中一般會對 MotionEvent.ACTION_DOWN
,MotionEvent.ACTION_UP
,MotionEvent.ACTION_MOVE
,MotionEvent.ACTION_CANCEL
分情況進行操作。
有去檢視原始碼中的事件攔截方法嗎?或者說在進行事件分發的時候如何讓正常的分發方式進行攔截?
我知道有個攔截事件的方法叫...叫,onInterceptEvent()
?應該是,不過由於平時專案較多,確實沒時間去關注太多原始碼。
厄,那你覺得在一個列表中,同時對父 View 和子 View 設定點選方法,優先響應哪個?為什麼會這樣?
肯定是優先響應子 View 的,至於為什麼這樣,平時知道這個結論,所以沒去太深入研究,但我相信我簡單看一下原始碼是肯定知道的。
先發表點扯淡
我們可能經常會遇到上面的這種情況,面試官希望瞭解我們知識的深入情況,或者說是平時學習慾望到底怎樣。可很不幸的是,我搞 模擬面試 以來,80% 的小夥伴都屬於開發能力不錯,可對類似事件分發這樣的基礎問題一概不知。究其原因,除去忙以外,大多數小夥伴還是覺得平時開發也用不上什麼,即使用到了,直接 Google 一下便能得到正確答案。
這大概就是很多人不會自定義 View 的原因吧,大多數效果在 GitHub 上都是現成的了,即使不太一樣,也可以簡單改改完事。
可很遺憾的是,我模擬面試那額外的 20% 的人,總拿到了令大多數人羨慕嫉妒恨的 offer,這不是沒有原因的。可能別人就平時的開發中保持了更多的一點求知慾,就學到了很多至關重要的細節知識。
正文
還是不能偏題,其實這樣的一個面試問題,確實是一個較為普遍的問題,我相信同型別的文章,網上一搜也是比比皆是,而且簡單看一下關注度就能知道有多少人倒在了這種原始碼型別的面試上。
一般情況下,事件列都是從使用者按下(ACTION_DOWN)的那一刻產生的,不得不提到,三個非常重要的與事件相關的方法。
- dispatchTouchEvent()
- onTouchEvent()
- onInterceptTouchEvent()
Activity 的事件分發機制
從英文單詞中已經很明顯的知道,dispatchTouchEvent()
是負責事件分發的。當點選事件產生後,事件首先會傳遞給當前的 Activity,這會呼叫 Activity 的 dispatchTouchEvent()
方法,我們來看看原始碼中是怎麼處理的。
注意截圖中,我增加了一些註釋,便於我們更加方便的理解,由於我們一般產生點選事件都是 MotionEvent.ACTION_DOWN
,所以一般都會呼叫到 onUserInteraction()
這個方法。我們不妨來看看都做了什麼。
很遺憾,這個方法實現是空的,不過我們可以從註釋和其他途徑可以瞭解到,該方法主要的作用是實現屏保功能,並且當此 Activity 在棧頂的時候,觸屏點選 Home、Back、Recent 鍵等都會觸發這個方法。
再來看看第二個 if 語句,getWindow().superDispatchTouchEvent()
,getWindow()
明顯是獲取 Window
,由於 Window
是一個抽象類,所以我們能拿到其子類 PhoneWindow
,我們直接看看 PhoneWindows.superDispatchTouchEvent()
到底做了什麼操作。
直接呼叫了 DecorView
的 superDispatchTrackballEvent()
方法。DecorView
繼承於 FrameLayout
,作為頂層 View,是所有介面的父類。而 FrameLayout
作為 ViewGroup
的子類,所以直接呼叫了 ViewGroup
的 dispatchTouchEvent()
。
ViewGroup 的事件分發機制
我們通過檢視 ViewGroup
的 dispatchTouchEvent()
可以發現。
注意其中紅框裡面的程式碼,看註釋也能知道,定義了一個 boolean 值變數 intercept
來表示是否要攔截事件。
其中採用到了 onInterceptTouchEvent(ev)
對 intercept
進行賦值。大多數情況下,onInterceptTouchEvent()
返回值為 false,但我們完全可以通過重寫 onInterceptTouchEvent(ev)
來改變它的返回值,不妨繼續往下看,我們後面對這個 intercept
做了什麼處理。
暫時忽略 判斷的 canceled
,該值同樣大多數時候都返回 false,所以當我們沒有重寫 onInterceptTouchEvent()
並使它的返回值為 true 時,一般情況下都是可以進入到該方法的。
繼續閱讀原始碼可以發現,裡面做了一個 For 迴圈,通過倒序遍歷 ViewGroup
下面的所有子 View,然後一個一個判斷點選位置是否是該子 View 的佈局區域,當然還有一些其他的,由於篇幅原因,這裡就不細講了。
View 的事件分發機制
ViewGroup
說到底還是一個 View,所以我們不得不繼續看看 View 的 dispatchTouchEvent()
。
紅框中的三個條件,第一個我就不用說了。
-
(mViewFlags & ENABLED_MASK) == ENABLED 該條件是判斷當前點選的控制元件是否為 enable,但由於基本 View 都是 enable 的,所以這個條件基本都返回 true。
-
mOnTouchListener.onTouch(this, event) 即我們呼叫
setOnTouchListener()
時必須覆蓋的方法onTouch()
的返回值。
從上述的分析,終於知道「onTouch()
方法優先順序高於 onTouchEvent(event)
方法」是怎麼來的了吧。
再來看看 onTouchEvent()
從上面的程式碼可以明顯地看到,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一個為 true,那麼 onTouchEvent()
就會返回 true 消耗這個事件。CLICKABLE 和 LONG_CLICKABLE 代表 View 可以被點選和長按點選,我們通常都會採用 setOnClickListener()
和 setOnLongClickListener()
做設定。接著在 ACTION_UP 事件中會呼叫 performClick()
方法,我們看看都做了什麼。
從截圖中可以看到,如果 mOnClickListener
不為空,那麼它的 onClick()
方法就會呼叫。
總結
本來寫到這就結束了,但回顧一遍還是打算給大家稍微總結一下。
需要總結的小點: 1、Android 事件分發總是遵循 Activity => ViewGroup => View 的傳遞順序; 2、
onTouch()
執行總優先於onClick()
原本想用文字總結的,結果發現簡書上還有這樣一篇神文:Android事件分發機制詳解:史上最全面、最易懂,所以直接引用一下其中的圖片。
-
Activity 的事件分發示意圖
-
ViewGroup 事件分發示意圖
-
View 的事件分發示意圖
-
事件分發工作流程總結
我是南塵,只做比心的公眾號,歡迎關注我。
做不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公眾號搜尋「nanchen」關注我的微信公眾號,目前多運營 Android ,儘自己所能為你提升。如果你喜歡,為我點贊分享吧~