寫在前面的話:
這篇文章是前Firefox Android工程師(現在跳槽去Facebook了) Lucas Rocha所寫,文中對Android中常用的四種自定義佈局方案進行了很好地分析,並結合這四種Android自定義佈局方案所寫的示例專案講解了它們各自的優劣以及四種方案之間的比較。看完這篇文章,也讓我對Android 自定義佈局有了進一步的瞭解,於是趁著興頭,我把它翻譯成中文,原文連結在此。
只要你寫過Android程式,你肯定使用過Android平臺內建的幾個佈局——RelativeLayout, LinearLayout, FrameLayout等等。 它們能幫助我們很好的構建Android UI。
這些內建的佈局已經提供了很多方便的構件,但很多情況下你還是需要來定製自己的佈局。
總結起來,自定義佈局有兩大優點:
- 通過減少view的使用和更快地遍歷佈局元素讓你的UI顯示更加有效率;
- 可以構建那些無法由已有的view實現的UI。
在這篇博文中,我將實現四種不同的自定義佈局,並對它們的優缺點進行比較。它們分別是: composite view, custom composite view, flat custom view, 和 async custom views。
這些程式碼實現可以在我的github上的 android-layout-samples 專案裡找到。這個app使用上面說到的四種自定義佈局實現了相同的UI效果。它們使用 Picasso 來載入圖片。這個app的UI只是twitter timeline的簡化版本——沒有互動,只有佈局。
好啦,我們先從最常見的自定義佈局開始吧: composite view。
Composite View
Composite views (也被稱為 compound views) 是眾多將多個view結合成為一個可重用UI元件的方法中最簡單的。這種方法的實現過程是這樣的:
- 繼承相關的內建的佈局。
- 在建構函式裡面填充一個 merge 佈局。
- 初始化成員變數並通過 findViewById()指向內部view。
- 新增自定義的API來查詢和更新view的狀態。
TweetCompositeViewcode 就是一個 composite view。它繼承於 RelativeLayout,並填充了 tweet_composite_layout.xmlcode 佈局檔案,最後向外界暴露了 update()方法來更新它在adaptercode裡面的狀態。
Custom Composite View
上面提到的TweetCompositeView 這種實現方式能滿足大部分的情況。但是碰到某些情況就不靈了。假設你現在想要減少子檢視的數量,讓佈局元素的便利更加有效。
這個時候我們可以回過頭來看看,儘管 composite views 實現起來比較簡單,但是使用這些內建的佈局還是有不少的開銷的——特別是 LinearLayout 和RelativeLayout這種比較複雜的容器。由於Android平臺內建佈局的實現,在一次佈局元素遍歷中,系統需要處理許多佈局的結合和子檢視的多次測量——LinearLayout的 layout_weight 的屬性就是常見例子。
因此你可以為你的app量身定做一套子檢視的計算和定位邏輯,這樣的話你就可以極大的優化你的UI了。這種做法就是我接下來要介紹的 custom composite view.
顧名思義,一個 custom composite view 就是一個重寫了onMeasure() 和onLayout() 方法的 composite view 。因此相比之前的composite view繼承了 RelativeLayout,現在我們需要更進一步——繼承更抽象的ViewGroup。
TweetLayoutViewcode 就是通過這種技術實現的。注意現在這個實現不像TweetComposiveView 繼承了LinearLayout ,這也就避免了 layout_weightcode這個屬性的使用了。
這個大費周折的過程通過ViewGroup’s 的measureChildWithMargins() 方法和背後的 getChildMeasureSpec() 方法計算出了每個子檢視的 MeasureSpec 。
TweetLayoutView 不能正確地處理所有可能的 layout 組合但是它也不必這樣。我們肯定需要根據特定需求來優化我們的自定義佈局,這種方式可以讓我們寫出簡單高效的佈局程式碼。
Flat Custom View
如你所見,custom composite views 可以簡單地通過使用ViewGroup 的API就可以實現了。大部分時候,這種實現是可以滿足我們的需求的。
然而我們想更進一步的話——優化我們應用中的關鍵部分UI,比如 ListViews ,ViewPager等等。如果我們把所有的 TweetLayoutView 子檢視合併成一個單一的自定義檢視然後統一管理會怎麼樣呢?這就是我們接下來要討論的 flat custom view——參看下面的圖片。
flat custom view 就是一個完全自定義的 view ,它完全負責內部的子檢視的計算,位置安排,繪製。所以它就直接繼承了View 而不是 ViewGroup。
如果你想找找現實生活中app是否存在這樣的例子,很簡單——開啟你手機“開發者模式”裡面的 “顯示佈局邊界”選項,然後開啟 Twitter, Gmail, 或者 Pocket這些app,它們在列表UI裡面都採用了 flat custom view。
使用 flat custom view最主要的好處就是可以極大地壓縮app 的檢視層級,進而可以進行更快的佈局元素遍歷,最終可以減少記憶體佔用。
Flat custom view 可以給你最大的自由,就好像你在一張白紙上面作畫。但是這樣的自由是有代價的:你不能使用已有的那些檢視元素了,比如 TextView 和 ImageView。沒錯,在 Canvas 上面描繪文字 的確很簡單,但要你實現 ellipsizing(就是對過長的文字截斷)呢?同樣, 在 Canvas 上面 描繪圖片確很簡單,但是如何縮放呢?這些限制同樣適用於touch events, accessibility, keyboard navigation等等。
所以使用flat custom view的底線就是:只將flat custom view應用於你的app的UI核心部分,其他的就直接依賴Android平臺提供的view了。
TweetElementViewcode 就是 flat custom view。為了更容易的實現它,我建立了一個小小的自定義檢視框架叫做UIElement。你可以在 canvascode 這個包裡找到它。
UIElement 提供了和Android平臺類似的 measure/layout API 。它包含了沒有影象介面的 TextView 和 ImageView ,這兩個元素包含了幾個必需的特性——分別參看 TextElementcode 和ImageElementcode 。它還擁有自己的 inflatercode ,幫助從 佈局資原始檔code裡面例項化UIElement 。
注意: UIElement 還處於非常早期的開發階段,所以還有很多缺陷,不過將來隨著不斷的改進UIElement 可能會變得非常有用。
你可能覺得TweetElementView 的程式碼看起來很簡單,這是因為實際程式碼都在 TweetElementcode裡面——實際上TweetElementView 扮演託管的角色code。
TweetElement 裡面的佈局程式碼和TweetLayoutView‘非常類似,但是它使用 Picasso 請求圖片時卻不一樣code ,因為TweetElement 沒有使用ImageView。
Async Custom View
總所周知,Android UI 框架時單執行緒的 。 這樣的單執行緒會帶來一些限制。比如,你不能在主執行緒之外遍歷佈局元素——然而這對複雜、動態的UI是很有益處的。
假如你的app 在一個ListView 中很佈局比較複雜的條目(就像大多數社交app一樣),那麼你在滑動ListView 就很有可能出現跳幀的現象,因為ListView 需要為列表中即將出現的新內容計算它們的檢視大小code和佈局code。同樣的問題也會出現在GridViews,ViewPagers等等。
如果我們可以在主執行緒之外的執行緒上面對那些還沒有出現的子檢視進行佈局遍歷是不是就可以解決上面的問題了?也就是說,在子檢視上面呼叫 measure()和layout() 方法都不會佔用主執行緒的時間了。
所以 async custom view 就是一個允許子檢視佈局遍歷過程發生在主執行緒之外的實驗,這個idea是受到Facebook的Paperteam async node framework 這個視訊激發所想到的。
既然我們在主執行緒之外永遠接觸不到Android平臺的UI元件,因此我們需要一個API在不能直接接觸到這個檢視的前提下對這個檢視的內容進行測量、佈局。這恰恰就是 UIElement 框架提供給我的功能。
AsyncTweetViewcode 就是一個 async custom view。它使用了一個執行緒安全的 AsyncTweetElementcode 工廠類code 來定義它的內容。具體過程是一個Smoothie 子項載入器code 在一個後臺執行緒上對暫時不可見的AsyncTweetElement 進行建立、預測量和快取(在記憶體裡面,以便後來直接使用)。
當然在實現這個非同步UI的過程中我還是妥協了一些,因為你不知道如何顯示任意高度的佈局佔位符。比如,當佈局非同步傳遞過來的時候你只能在後臺執行緒對它們的大小進行一次更改。因此當一個 AsyncTweetView 就要顯示的時候卻無法在記憶體裡面找到合適的AsyncTweetElement ,這個時候框架就會強制在主執行緒上面建立一個AsyncTweetElement code。
還有,預先載入的邏輯和記憶體快取過期時間設定都需要比較好的實現來保證在主執行緒儘可能多地利用記憶體裡面的快取佈局。比如,這個方案中使用 LRU 快取code 就不是一個明智的選擇。
儘管還存在這些限制,但是使用 async custom view 的得到的初步結果還是很有前途的。當然我也會通過重構這個UIElement 框架和使用其他類別的UI在這個領域繼續探索。讓我們靜觀其變吧。
總結
在我們涉及到佈局的時候,我們自定義的越深,我們能從Android平臺所能獲得的依賴就越少。所以我們也要避免過早優化,只在確實能實實在在改善app質量和效能的區域進行完全的佈局自定義。
這不是一個非黑即白的決定。在使用平臺提供的UI元素和完全自定義的兩種極端之間還有很多方案——從簡單的composite views 到複雜的 async views。實際專案中,你可能會結合文中的幾種方案寫出優秀的app。