深入解析Android的自訂布局
寫在前面的話:
這篇文章是前Firefox Android工程師現在跳槽去Facebook了) Lucas Rocha所寫,文中對Android中常用的四種自訂布局方案進行了很好地分析,並結合這四種Android自訂布局方案所寫的樣本項目講解了它們各自的優劣以及四種方案之間的比較。看完這篇文章,也讓我對Android 自訂布局有了進一步的瞭解,於是趁著興頭,我把它翻譯成中文,原文連結在此。
只要你寫過Android程式,你肯定使用過Android平台內建的幾個布局——RelativeLayout, LinearLayout, FrameLayout等等。 它們能協助我們很好的構建Android 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組件的方法中最簡單的。這種方法的實現過程是這樣的:
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——參看下面的圖片。
左邊為CUSTOM COMPOSITE VIEW ,右邊是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是很有益處的。
假如你的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。