**1.**每次完整的事件分發流程,都包含自上而下的 “遞”,和自下而上的 “歸” 2 個流程。
**2.**每次完整的事件分發流程,都是針對一個事件(MotionEvent)完成的遞歸,而一個事件隻對應着一個 ACTION,例如 ACTION_DOWN。
**3.**一次使用者觸摸操作,我們稱之為一個事件序列。一個事件序列會包含 ACTION_DOWN、ACTION_MOVE … ACTION_MOVE、ACTION_UP 等多個事件。(其中 ACTION_MOVE 的數量是從 0 到多個不等)
也即一個事件序列,包含從 ACTION_DOWN 到 ACTION_UP 的多次事件分發流程。
下面我用一張圖概括 View 事件分發的遞和歸流程。

如圖所示:👆👆👆
事先分發包含 3 個重要方法:
dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。
因而首先,在遞的過程中,目前層級是執行 child.dispatchTouchEvent:
- 如果 child 是 ViewGroup,那麼實際執行的就是 ViewGroup 重寫的 dispatchTouchEvent 方法。該方法内可以判斷,是否在目前層級攔截目前事件、或是遞給下一級。
- 如果 child 是不再有 child 的 View 或 ViewGroup,那麼實際執行的就是 View 類實作的 super.dispatchTouchEvent 方法。該方法内可以判斷,如果 View enabled 并且實作了 onTouchListener,且 onTouch 傳回 true,那麼不執行 onTouchEvent,并直接傳回結果。否則執行 onTouchEvent。
此外,在 onTouchEvent 中如果 clickable 并且實作了 onClickListener 或 onLongClickListener,那麼會執行 onClick 或 onLongClick。
總之,走到沒有 child 的層級,即意味着步入“歸”流程,如果該層級的 super.dispatchTouchEvent 沒有傳回 true,那麼将繼續執行上一級的 super.dispatchTouchEvent,直到被某一級消費,也即傳回 true 了為止。
上面我們介紹了正常流程下,所會執行到的方法,包括 View 實作的 dispatchTouchEvent,ViewGroup 重寫的 dispatchTouchEvent,以及 onTouchEvent。
如圖。👆👆👆
其實在事件的 “遞” 流程中,ViewGroup 可以在目前層級,通過設定 onInterceptTouchEvent 方法傳回 true,來攔截事件的下發,而直接步入“歸”流程。
正所謂 “上有正策、下有對策”。在 ViewGroup 可以攔截事件下發的同時,child 也可以通過 getParent.requestDisallowInterceptTouchEvent 方法,來阻止上一級的下發攔截。
額外需要明确的 3 個小細節
細節1:明确消費的概念
要将 “消費” 和 “執行” 這兩個概念明确區分開。
網上的内容總讓人誤以為,目前層級不消費,就是不執行 super.dispatchTouchEvent 了。
事實上,不消費,簡單地了解就是,“事情做了、隻是結果不 OK” —— 在歸流程中,如果目前層級的 super.dispatchTouchEvent return true 了,那麼再往上的層級都不再執行自己的 super.dispatchTouchEvent,而是直接 return true。并且,目前層級的下級,都執行過 super.dispatchTouchEvent,隻是結果傳回了 false 而已。
細節2:明确攔截的作用
網上的内容總是讓人誤以為,目前層級攔截了,就直接在目前層級消費了。
實際上,目前層級攔截了,隻是提前結束了 “遞” 流程,并從目前層級步入 “歸” 流程而已。具體判定是在哪個層級被消費,還是根據 <細節1> 的名額:看在哪個層級的 super.dispatchTouchEvent return true。
細節3:攔截方法隻走一次,不代表攔截隻走一次
網上的内容總是讓人誤以為,本次 ACTION_DOWN 被攔截了,那麼往後的 ACTION_MOVE 和 ACTION_UP 都不被攔截了。
實際上,是 onInterceptTouchEvent 方法隻走一次,一旦走過,就會留下記号(mFirstTouchTarget == null)那麼下一次直接根據這個記号來判斷攔不攔截。
為什麼這麼設計呢?因為一連串的事件序列,要求在幾百微秒内完成。如果每次都完整走一遍方法,那豈不耽誤事?是以本着 “能省即省” 的原則,凡是已确認會攔截的,後續就不再走方法判斷,而是直接走變量标記來判斷。
到此已經講完 3 個細節了,要不要再講 2 個呢?
講?不講?講?不講?
好嘛,再講 2 個 ~
細節4:ACTION_DOWN 不執行,那麼沒下次了
這個很好了解,和 <細節3> 同理。
連事件序列的第一個事件都不接了(父容器走後續事件的分發時發現 mFirstTouchTarget == null),那就意味着不接了呗 —— 那後續的活就不會交給你了(不會再走你的 super.dispatchTouchEvent 來試探),直接根據變量标記(mFirstTouchTarget == null)做出判斷,“能省即省”。
細節5:内部攔截并不能阻止父容器對 ACTION_DOWN 的處理
也即在 child 的 onTouch、onTouchEvent 中調用 getParent.requestDisallowInterceptTouchEvent 時,被設計為對父容器的 ACTION_DOWN 無效 —— 在父容器 dispatchTouchEvent 時,會首先重置 mGroupFlags。( ViewGroup 正是根據 mGroupFlags 是否包含 FLAG_DISALLOW_INTERCEPT 來判斷是否不攔截的)
為什麼這麼設計呢?
這個問題讀者可以想一想,歡迎在評論區留言 ~
總結
- View 事件分發的本質是遞歸。
- 遞歸的本質是,任務的下發和結果的上報。
- View 事件分發設計成遞歸,是為了配合 View 的排版規則,形成符合使用者直覺的觸控體驗。
- View 事件分發的對象是一個 MotionEvent。
- 一次使用者觸控操作包含多個 MotionEvent(例如從 ACTION_DOWN 到 ACTION_UP ),也即會走多次事件分發流程。
- 一次 View 事件分發流程包含 “遞” 流程和 “歸” 流程,“遞” 流程可以因 ViewGroup 的攔截而提前步入 “歸” 流程。
- child 可以通過 getParent.requestDisallowInterceptTouchEvent 阻止父容器的攔截。因而需要差異化地配置門檻值,來確定 child 執行 getParent.requestDisallowInterceptTouchEvent 優先于父容器 onInterceptTouchEvent 傳回 true(不然都先被攔截了,child 哪有機會阻止?)
- 在“歸”流程中,唯有目前層級的 super.dispatchTouchEvent 傳回了 true,才認定被消費,被消費前,下級都有幹活,隻是結果不 OK。被消費後,上級都不需要幹活,直接向上傳達消費者的功。
這樣說,你了解了嗎?
最後
如果你覺得文章寫得不錯就給個贊呗?如果你覺得那裡值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。
希望讀到這的您能轉發分享和關注一下我,以後還會更新技術幹貨,謝謝您的支援!
轉發+點贊+關注,第一時間擷取最新知識點
Android架構師之路很漫長,一起共勉吧!
以下牆裂推薦閱讀!!!
- Android學習筆記參考(敲黑闆!!)
- “寒冬未過”,阿裡P9架構分享Android必備技術點,讓你offer拿到手軟!
- 畢業3年,我是如何從年薪10W的拖拽工程師成為30W資深Android開發者!
- 騰訊T3大牛帶你了解 2019 Android開發趨勢及必備技術點!
- 八年Android開發,從碼農到架構師分享我的技術成長之路,共勉!
最後祝大家生活愉快~
技術點!]( )
- 八年Android開發,從碼農到架構師分享我的技術成長之路,共勉!
最後祝大家生活愉快~
[外鍊圖檔轉存中…(img-DkPKUZin-1630941615066)]