很高興見到你!java
今天我要和你們分享的是 View 事件分發。算法
本來這是 《重學安卓》專欄原定的第 8 篇(鑑於讀者的強烈要求,暫時插播了 Jetpack 系列),可沒想到的是,我沒法忘卻 3 年前備受折磨的那個夜晚 —— 在我第一次學習 View 事件分發,卻被網文折磨的那個夜晚。學習
是網上介紹 View 事件分發的文章不夠多嗎?spa
不是的,偏偏相反,網上的爆款文章不可勝數,待你仔細閱讀,卻 很有一種「外地人上了黑車」的感受 —— 一言不合先上 30 張圖表,帶你在城市外圍饒個上百圈,就是不直奔主題 解釋一個現象爲何會存在、形成它存在的原因爲什麼、它如此設計是爲了解決什麼問題 ……設計
比起 撥開迷霧、明確情況、創建感性認識,他們更熱衷於自我包裝。3d
—— 有沒有幫助我無論,先唬住人再說。code
爲了唬人,就算給他人徒添困擾、白費大量時間,也在所不惜!cdn
正是對那次痛苦經歷的念念不忘,因而我 破例 將這篇文章分享給你們。xml
在此,我向 3 年前的那個本身發誓,我必在 結尾 200 字 就講明白,別人非要繞個 3000、5000 字都講不明白的事件分發。對象
不只如此,我還要額外地幫助你們理解,事件分發流程中的 3 個小細節:之因此如此設計,是出於什麼考慮。經過「知其因此然」,來方便你們更好地加深印象。
😉
此外,已經訂閱專欄的小夥伴請不要擔憂,本文僅僅是介紹 View 事件分發機制的基礎。至於滑動衝突等現實問題的解決,好戲還在後頭 ~
還沒閱讀的小夥伴也請不要着急,正由於今天講的是基礎,光是看了這一篇,你也沒白來!
什麼是遞歸呢?遞歸的本質是什麼呢?
顧名思義,遞歸是一種包含 「遞」 流程和 「歸」 流程的算法。當咱們在找尋目標時,即是處於 「遞」 流程,當咱們找到目標,打算從目標開始來執行事務時,咱們便開啓了 「歸」 流程。
若是這麼說有點抽象的話,不妨結合現實中的實例來理解下遞歸:
案例:職場任務的下發和上報,就是典型的遞歸
領導 自上而下、逐級地下達任務、尋找目標執行者,這就是 「遞」 流程。
直到找到合適的執行者時,便開啓了 自下而上 的 「歸」流程。若當前執行者沒法讓結果 OK,那麼上報給他的上級,由他的上級來執行,若是上級也不 OK,那麼繼續向上,直到結果 OK 爲止。
僞代碼來表示,即:
boolean dispatch() {
if (hasTargetChild && child.dispatch()) {
return true;
} else {
return executeByMySelf();
}
}
複製代碼
如此設計,是爲了與 View 的排版相呼應。
View 的排版規則是:嵌套越深的,顯示層級越高。而顯示層級越高,就越容易覆蓋層級低的、被用戶看見。
再加上,「所見即所得」,要求 「用戶看到了什麼,觸控到的也該是什麼」(簡言之,操做要符合用戶直覺)。
所以,正是考慮到嵌套越深,層級越高,觸摸也一般會是交給層級高的來處理,於是也將事件分發設計成遞歸。
由於這符合常理。越外層的,做爲父容器而充當背景,越裏層的,做爲子控件而至於前景。
<LinearLayout>
<ScrollView>
<TextView/>
</ScrollView>
</LinearLayout>
複製代碼
首先咱們要明確的 3 點是:
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。
經過前面的 《重學安卓:Activity 的快樂你不懂!》 咱們知道,View 和 ViewGroup 是組合模式的關係,於是 ViewGroup 爲了分發的須要,會重寫一些 View 的方法,就包括這裏的 dispatchTouchEvent。
於是首先,在遞的過程當中,當前層級是執行 child.dispatchTouchEvent:
此外,在 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 方法,來阻止上一級的下發攔截。
(具體會在下一篇《滑動衝突處理》中介紹)
要將 「消費」 和 「執行」 這兩個概念明確區分開。
網上的內容總讓人誤覺得,當前層級不消費,就是不執行 super.dispatchTouchEvent 了。
事實上,不消費,簡單地理解就是,「事情作了、只是結果不 OK」 —— 在歸流程中,若是當前層級的 super.dispatchTouchEvent return true 了,那麼再往上的層級都再也不執行本身的 super.dispatchTouchEvent,而是直接 return true。而且,當前層級的下級,都執行過 super.dispatchTouchEvent,只是結果返回了 false 而已。
網上的內容老是讓人誤覺得,當前層級攔截了,就直接在當前層級消費了。
實際上,當前層級攔截了,只是提早結束了 「遞」 流程,並從當前層級步入 「歸」 流程而已。具體斷定是在哪一個層級被消費,仍是根據 <細節1> 的指標:看在哪一個層級的 super.dispatchTouchEvent return true。
網上的內容老是讓人誤覺得,本次 ACTION_DOWN 被攔截了,那麼日後的 ACTION_MOVE 和 ACTION_UP 都不被攔截了。
實際上,是 onInterceptTouchEvent 方法只走一次,一旦走過,就會留下記號(mFirstTouchTarget == null)那麼下一次直接根據這個記號來判斷攔不攔截。
爲何這麼設計呢?由於一連串的事件序列,要求在幾百微秒內完成。若是每次都完整走一遍方法,那豈不耽誤事?因此本着 「能省即省」 的原則,凡是已確認會攔截的,後續就再也不走方法判斷,而是直接走變量標記來判斷。
到此已經講完 3 個細節了,要不要再講 2 個呢?
講?不講?講?不講?
好嘛,再講 2 個 ~
這個很好理解,和 <細節3> 同理。
連事件序列的第一個事件都不接了(父容器走後續事件的分發時發現 mFirstTouchTarget == null),那就意味着不接了唄 —— 那後續的活就不會交給你了(不會再走你的 super.dispatchTouchEvent 來試探),直接根據變量標記(mFirstTouchTarget == null)作出判斷,「能省即省」。
也即在 child 的 onTouch、onTouchEvent 中調用 getParent.requestDisallowInterceptTouchEvent 時,被設計爲對父容器的 ACTION_DOWN 無效 —— 在父容器 dispatchTouchEvent 時,會首先重置 mGroupFlags。( ViewGroup 正是根據 mGroupFlags 是否包含 FLAG_DISALLOW_INTERCEPT 來判斷是否不攔截的)
爲何這麼設計呢?
這個問題讀者能夠想想,歡迎在評論區留言 ~
咱們會在下一篇,滑動衝突實戰中介紹。
這樣說,你理解了嗎?
看不過癮?這裏只爲你 而準備了一份 簡潔有力的 《重學安卓》認知地圖 😉