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