資深安卓程序員帶你用另一種角度學習 View 事件分發!

我無法忘卻 3 年前備受折磨的那個夜晚 —— 在我第一次學習 View 事件分發,卻被網文折磨的那個夜晚。

資深安卓程序員帶你用另一種角度學習 View 事件分發!

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

不是的,恰恰相反,網上的爆款文章不計其數,待你仔細閱讀,卻 頗有一種“外地人上了黑車”的感覺 —— 一言不合先上 30 張圖表,帶你在城市外圍饒個上百圈,就是不直奔主題 解釋一個現象爲什麼會存在、造成它存在的緣由爲何、它如此設計是爲了解決什麼問題 ……

資深安卓程序員帶你用另一種角度學習 View 事件分發!

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

—— 有沒有幫助我不管,先唬住人再說。

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

資深安卓程序員帶你用另一種角度學習 View 事件分發!

正是對那次痛苦經歷的念念不忘,於是我將這篇文章分享給大家。

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

不僅如此,我還要額外地幫助大家理解,事件分發流程中的 3 個小細節:之所以如此設計,是出於什麼考慮。通過“知其所以然”,來方便大家更好地加深印象。

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

View 事件分發的本質是遞歸!

資深安卓程序員帶你用另一種角度學習 View 事件分發!

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

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

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

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

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

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

僞代碼來表示,即:

boolean dispatch(){
    if (hasTargetChild) {
        return child.dispatch();
    } 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 事件分發的遞和歸流程:

資深安卓程序員帶你用另一種角度學習 View 事件分發!

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

dispatchTouchEventonInterceptTouchEventonTouchEvent

我們知道,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 了爲止。

資深安卓程序員帶你用另一種角度學習 View 事件分發!

上面我們介紹了正常流程下,所會執行到的方法,包括 View 實現的 dispatchTouchEvent,ViewGroup 重寫的 dispatchTouchEvent,以及 onTouchEvent。

其實在事件的 “遞” 流程中,ViewGroup 可以在當前層級,通過設置 onInterceptTouchEvent 方法返回 true,來攔截事件的下發,而直接步入“歸”流程。

正所謂 “上有正策、下有對策”。在 ViewGroup 可以攔截事件下發的同時,child 也可以通過 getParent.requestDisallowInterceptTouchEvent 方法,來阻止上一級的下發攔截。

資深安卓程序員帶你用另一種角度學習 View 事件分發!

額外需要明確的5個小細節

細節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)那麼下一次直接根據這個記號來判斷攔不攔截。

爲什麼這麼設計呢?因爲一連串的事件序列,要求在幾百微秒內完成。如果每次都完整走一遍方法,那豈不耽誤事?所以本着 “能省即省” 的原則,凡是已確認會攔截的,後續就不再走方法判斷,而是直接走變量標記來判斷。

細節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 的排版規則,形成符合用戶直覺的觸控體驗。
  • View 事件分發的對象是一個 MotionEvent。
  • 一次用戶觸控操作包含多個 MotionEvent(例如從 ACTION_DOWN 到 ACTION_UP ),也即會走多次事件分發流程。
  • 一次 View 事件分發流程包含 “遞” 流程和 “歸” 流程,“遞” 流程可以因 ViewGroup 的攔截而提前步入 “歸” 流程。
  • child 可以通過 getParent.requestDisallowInterceptTouchEvent 阻止父容器的攔截。因而需要差異化地配置閾值,來確保 child 執行 getParent.requestDisallowInterceptTouchEvent 優先於父容器 onInterceptTouchEvent 返回 true(不然都先被攔截了,child 哪有機會阻止?)
  • 在“歸”流程中,唯有當前層級的 super.dispatchTouchEvent 返回了 true,才認定被消費,被消費前,下級都有幹活,只是結果不 OK。被消費後,上級都不需要幹活,直接向上傳達消費者的功。

這樣說,你理解了嗎?

最後對於程序員來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!

這裏附上上述的技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,這裏以圖片的形式給大家展示一部分。

相信它會給大家帶來很多收穫:

資深安卓程序員帶你用另一種角度學習 View 事件分發!

上述【高清技術腦圖】以及【配套的架構技術PDF】可以 加我wx:X1524478394 免費獲取

當程序員容易,當一個優秀的程序員是需要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章