解析 ViewTreeObserver 源碼(下)
繼上篇內容,本文介紹 ViewTreeObserver 的使用,以及體會其所涉及的觀察者模式,期間會附帶回顧一些基礎知識。最後,我們簡單聊一下 Android 的消息傳遞,附高清示意圖,輕松捋清整個傳遞過程!
在開始下篇之前,有必要回顧一下上篇《解析 ViewTreeObserver 源碼,體會觀察者模式、Android消息傳遞(上)》提及的 ViewTreeObserver 的概念:
ViewTreeObserver 是被用來註冊監聽視圖樹的觀察者,在視圖樹發生全局改變時將收到通知。這種全局事件包括但不限於:整個視圖樹的布局發生改變、在視圖開始繪制之前、視圖觸摸模式改變時…
還沒有看上篇,或者對上篇已經沒印象的,建議先去看一下。
本篇內容較多,為節省篇幅,直接接著上篇繼續講。
#1. 一覽 ViewTreeObserver 的大綱
先通過這部分來對類的構成進行粗略的認知,這樣才能自如的應對後面的內容。本部分建議大家參考源碼去看,這樣會更直觀、更容易理解,我參考的源碼是 Android 6.0 的 SDK(api 23)。
查看類的大綱發現,該類看著挺復雜,但概括起來看就很簡單了,下面我們按類別來一個個拿下。(windows 下 AS 查看類大綱的默認快捷鍵是 Ctrl + F12,大綱模式下還支持搜索以快速定位)
1.1 類的接口
ViewTreeObserver 通過接口回調的方式實現觀察者模式,當接收到通知後,通過接口的回調方法告知程序相應的事件發生了。在 ViewTreeObserver 中,包含了 11 個接口,對應著11中觀察事件,如下圖:
這裏寫圖片描述
1.2 類的方法
介紹完接口,下面總結一下 ViewTreeObserver 類的方法,大概分為以下四種類型。
添加監聽:addOnXxxListener(OnXxxListener)
移除監聽:removeOnXxxListener(OnXxxListener)
分發事件:dispatchOnXxx()
其他方法:checkIsAlive()、isAlive()方法等
“其他方法”在上篇差不多提過了,現在我們著重看前三類方法,下面簡稱 add、remove 和 dispatch 方法。
查看類可知,對於前面那張圖所展示的每一個接口,都有與其對應的 add、remove、dispatch 方法。舉個例子吧,以 OnGlobalLayoutListener(全局布局監聽) 為例,那麽與其對應的三類方法就是:
addOnGlobalLayoutListener(OnGlobalLayoutListener listener);
removeOnGlobalLayoutListener(OnGlobalLayoutListener victim);
dispatchOnGlobalLayout();
這麽說,一共有11個接口,那麽與之對應的 add、remove、dispatch 方法也就分別有11個,沒錯,我們通過大綱查看時就是這樣。這個大家自行去類中查看,或者根據上面舉的例子類推一下,我就不再貼代碼了。
下面補充一點與方法的使用相關的內容:
雖說 ViewTreeObserver 包含這麽多方法,但是系統並沒有對我們開放所有的API。我們可以驗證一下,在程序代碼中先通過 getViewTreeObserver() 獲取 View 的 ViewTreeObserver 對象,然後使用該對象分別調用這幾類方法,分別模糊匹配 add、remove 和 dispatch,然後查看IDE的智能提示。
先看看調用 add 和 remove 方法:
如圖所示,add 和 remove 方法只分別只有8個,並沒有11個。其中remove中最後一個方法removeGloableOnLayoutListener已經過時了,在 API 16 取代它的方法是removeOnGloableLayoutListener。查看removeGloableOnLayoutListener方法可知,其直接調用了removeOnGloableLayoutListener方法,功能上沒區別。區別在於名字,肯定是初期方法命名不合理,後來想改,但又不能直接修改或刪除。所以,在一開始就設計好一些規範,並在開發過程中按照代碼規範開發,是有多重要…
既然都是8個,那各自少掉的3個呢?進 ViewTreeObserver類一看,發現不讓外部調用的是與OnWindowShownListener、OnComputeInternalInsetsListener、OnEnterAnimationCompleteListener接口對應的add、remove方法,這幾個方法之所以在程序中無法訪問,是因為被添加了 @hide標簽,這是什麽?
@hide 意味著被其標記的方法、類或者變量,在自動生成文檔時,將不會出現在API文檔中對開發者開放,但是系統可以調用,這就解釋了為什麽我們只能訪問其中8個方法了。其中有些要求對版本有要求,例如添加或移除 OnWindowAttachListener,需要 API 18 以上,而我們一版在開發時會選擇最低適配 Android 4.0,也即是 API 為 14,這樣一來就無法使用。
其實,可以通過反射訪問被 @hide 標記的域。但是不建議這麽做,因為 Google 在添加該標記時解釋道:
We are not yet ready to commit to this API and support it,so @hide。
既然沒有準備好提交這個API並支持他,也就意味著 Google 可能會隨時修改這些方法(雖然可能性很小),所以出於保險還是不要通過反射使用的好(個人觀點)。
再來看看 dispatch 方法可用的有哪些:
喔,居然只有3個!查看 ViewTreeObserver 類,發現其余8個不可訪問的方法沒有聲明修飾符,那就是默認的 default 類型。我們知道,default 修飾的方法只能在同一包內可見,ViewTreeObserver.java 在 android.view 包下,我們在程序中顯然無法訪問。
#2. 接口和方法的作用
為了保持內容的連貫和思路的清晰,在上一節只是介紹了 ViewTreeObserver 類的構成,並沒有解釋具體的作用。下面趁熱打鐵,看一下各自的作用。此處仍以 OnGlobalLayoutListener(全局布局監聽) 接口對應的三個方法為例,其他接口的原理都一樣,不再贅述。
2.1 OnGlobalLayoutListener 接口:
註釋很精確的概括了其作用:當全局布局狀態,或者視圖樹的子view可見性發生改變時,將調用該回調接口。
該接口包含了一個回調方法 onGlobalLayout(),我們在程序中就是通過覆寫該方法,實現自己的邏輯,具體使用將在實戰部分介紹。
##2.2 addOnGlobalLayoutListener 和 removeOnGlobalLayoutListener 方法
還是將這倆好基友放在一塊介紹,我直接簡稱 add 和 remove 了。
在程序中,通過 add 方法添加一個對 view 布局發生改變的監聽,傳入 OnLayoutGlobalListener 接口對象,覆寫接口的 onGlobalLayout() 方法,系統會將我們傳入的 OnLayoutGlobalListener 存在集合中。
當通過 add 監聽之後,我們需要在適當的時候通過 remove 方法移除該監聽,防止多次調用。通常在覆寫的 onGlobalLayout() 時方法中調用 remove 方法移除監聽。
##2.3 dispatchOnGlobalLayout 方法
dispatch 方法一般都由系統調用,我們不需要去關心。在 dispatchOnGlobalLayout 方法中,會遍歷存放 OnLayoutGlobalListener 對象的集合,然後調用 OnLayoutGlobalListener 對象的 onGlobalLayout() 方法,通知程序該事件發生了。
[註:上述代碼中存放 OnGlobalLayoutListener 的集合 CopyOnWriteArray,值得了解一下,會讓你受益匪淺。本打算講的,但限於篇幅只好作罷,感興趣的可以上網了解一下]
3.使用姿勢(實戰)
到目前為止,我們對 ViewTreeObserver 的認識仍停留在概念級別,終於等到了實戰環節,驗收自己學習成果的時刻到了。
##3.1 使用流程
我們還是先以 OnGlobalLayoutListener 為例介紹一下標準使用流程,這裏需要結合上篇所學內容。
通過 View 對象的 getViewTreeObserver() 獲取 ViewTreeObserver 對象。
檢測 observer 是否可用。不可用的話再獲取一次
定義 OnGlobalLayoutListener 接口對象 listener,並覆寫 onGlobalLayout() 回調方法。如果只監聽一次,記得在方法最後調用 observer.removeOnGlobalLayoutListener() 移除監聽,避免重復調用。
observer.addOnGlobalLayoutListener(listener) ,至此完成對該 View 對象的全局布局監聽。
附上一張不完整的流程圖,使用在線工具 ProcessOn 畫的,挺好用的,推薦給大家:
##3.2 實際使用
上面只是標準使用流程,實際開發中我們不會這麽多約束,下面看兩個實際的例子。值得註意的是,我們一直所說的 View,實際上指的是 View 及其子類,比如 LinearLayout、ImageView、TextView等。
① 在 onCreate() 中獲取 View 的高度
在開發中,我們有時需要在 onCreate() 方法中拿到一個view(任何View的子類)的寬高,這時候我們直接通過 getWidth() 和 getHeight() 方法獲取的值均為 0,因為真正的值要在 view 完成 onLayout() 之後才可以返回。這時,我們就可以借助 OnGlobalLayoutListener 監聽 view 的布局改變,當 view 布局發生改變且完成 onLayout() 後,就會調用 dispatchOnGlobal() 通知我們,接下來就會走到回調方法 onGlobalLayout() 中去。
view.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//1. do sth you want
width = view.getWidth();
height = view.getHeight;
Log.d("OnGlobalLayoutListener", "width:" + width + ",height:" + height);
//2. remove listener
// api 小於 16
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN){
//使用過時方法
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// api >= 16
else {
//使用替換方法
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
代碼已經寫得很清楚了,下面再補充兩點:
因為每次都是通過 getViewTreeObserver() 直接獲取 View 當前的observer,所以就沒再使用 isAlive() 判斷。
在介紹 remove 方法時,提到 removeGlobalOnLayoutListener() 方法已經過時,取而代之的是 removeOnGlobalLayoutListener() 方法。後者是在 JELLY_BEAN 版本才引入的,對應的 api 是 16。由於我當前程序的 minSdkVersion 為 14,所以需要根據實際版本號分開處理。其實,在本例中,是不需要分開處理的,我們直接調用已過時的 removeGlobalOnLayoutListener() 方法即可,因為在前面分析過,二者僅僅是名字上的差別。但我之所以這麽做,就是為了演示如何判斷版本號並據此選擇對應的方案。畢竟有些方法系統只提供了高版本的實現,之前的版本就沒有對應的方法,此時我們就必須自己實現在低版本上的功能了。
除了 OnGlobalLayoutListener,我們還可以借助 OnPreDrawListener 實現上述功能。同時,OnPreDrawListener 還可以幫助我們實現 View 初始化加載時的動畫效果。下面再舉個例子,供大家參考以熟悉api,實際的開發中需要靈活運用。
② View 的初始化加載動畫
直接上代碼,在 onCreate() 方法中:
添加屬性動畫:
最終效果:
---------------------
https://blog.csdn.net/my_truelove/article/details/52653072
解析 ViewTreeObserver 源碼(下)