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