重學安卓:學習 View 事件分發,就像外地人上了黑車!

前言

很高興見到你!java

今天我要和你們分享的是 View 事件分發。算法

本來這是 《重學安卓》專欄原定的第 8 篇(鑑於讀者的強烈要求,暫時插播了 Jetpack 系列),可沒想到的是,我沒法忘卻 3 年前備受折磨的那個夜晚 —— 在我第一次學習 View 事件分發,卻被網文折磨的那個夜晚。學習

是網上介紹 View 事件分發的文章不夠多嗎?spa

不是的,偏偏相反,網上的爆款文章不可勝數,待你仔細閱讀,卻 很有一種「外地人上了黑車」的感受 —— 一言不合先上 30 張圖表,帶你在城市外圍饒個上百圈,就是不直奔主題 解釋一個現象爲何會存在、形成它存在的原因爲什麼、它如此設計是爲了解決什麼問題 ……設計

比起 撥開迷霧、明確情況、創建感性認識,他們更熱衷於自我包裝。3d

—— 有沒有幫助我無論,先唬住人再說。code

爲了唬人,就算給他人徒添困擾、白費大量時間,也在所不惜!cdn

正是對那次痛苦經歷的念念不忘,因而我 破例 將這篇文章分享給你們。xml

在此,我向 3 年前的那個本身發誓,我必在 結尾 200 字 就講明白,別人非要繞個 3000、5000 字都講不明白的事件分發。對象

不只如此,我還要額外地幫助你們理解,事件分發流程中的 3 個小細節:之因此如此設計,是出於什麼考慮。經過「知其因此然」,來方便你們更好地加深印象。

😉

此外,已經訂閱專欄的小夥伴請不要擔憂,本文僅僅是介紹 View 事件分發機制的基礎。至於滑動衝突等現實問題的解決,好戲還在後頭 ~

還沒閱讀的小夥伴也請不要着急,正由於今天講的是基礎,光是看了這一篇,你也沒白來

View 事件分發的本質是遞歸

什麼是遞歸呢?遞歸的本質是什麼呢?

顧名思義,遞歸是一種包含 「遞」 流程和 「歸」 流程的算法。當咱們在找尋目標時,即是處於 「遞」 流程,當咱們找到目標,打算從目標開始來執行事務時,咱們便開啓了 「歸」 流程。

若是這麼說有點抽象的話,不妨結合現實中的實例來理解下遞歸:

案例:職場任務的下發和上報,就是典型的遞歸

領導 自上而下、逐級地下達任務、尋找目標執行者,這就是 「遞」 流程。

直到找到合適的執行者時,便開啓了 自下而上 的 「歸」流程。若當前執行者沒法讓結果 OK,那麼上報給他的上級,由他的上級來執行,若是上級也不 OK,那麼繼續向上,直到結果 OK 爲止。

僞代碼來表示,即:

boolean dispatch() {
    if (hasTargetChild && child.dispatch()) {
        return true;
    } else {
        return executeByMySelf();
    }
}
複製代碼

View 事件分發爲什麼要設計成遞歸呢?

如此設計,是爲了與 View 的排版相呼應。

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 事件分發的遞和歸流程。

事件分發流程.png

如圖所示:👆👆👆

事先分發包含 3 個重要方法:

dispatchTouchEventonInterceptTouchEventonTouchEvent

經過前面的 《重學安卓:Activity 的快樂你不懂!》 咱們知道,View 和 ViewGroup 是組合模式的關係,於是 ViewGroup 爲了分發的須要,會重寫一些 View 的方法,就包括這裏的 dispatchTouchEvent。

於是首先,在遞的過程當中,當前層級是執行 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 了爲止。

事件分發流程.png

上面咱們介紹了正常流程下,所會執行到的方法,包括 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。被消費後,上級都不須要幹活,直接向上傳達消費者的功。

這樣說,你理解了嗎?

xzl短

看不過癮?這裏只爲你 而準備了一份 簡潔有力的 《重學安卓》認知地圖 😉

相關文章
相關標籤/搜索