Android最佳實踐之UI篇
引子
不管進行什么開發,桌面也好、移動端也罷,UI一直都是讓人頭大的一部分。那對于Android開發來說,在UI這一塊,是否有什么最佳實踐能讓人少走一些彎路嗎?這兩天就這個問題搜了一圈,收獲了不少。
UI最佳實踐的N條建議
1. 避免嵌套過多層級的布局</h2>
1. 避免嵌套過多層級的布局</h2>
即使使用的全都是官方提供的基礎布局和控件,也不意味著就能做出高效的UI布局設計。每個布局(layout),控件(Button、TextView等),都需要進行初始化,測量大小、定位以及繪制。布局里嵌套了過多的層級將帶來相當大的性能開銷。官方提供了Hierarchy Viewer工具來幫助我們查找可能的優化點。Hierarchy Viewer的使用方式這里就不作介紹了,官方文檔說得很清楚。通過它我們能查看各個頁面的布局層次和以及各個步驟(測量、定位、繪制)的耗時,并根據這些數據做出相應的優化。(使用文檔傳送門)
使用線性布局(LinearLayout)來組織界面是導致層級過多的主要原因,由于這種組織方式相當直觀,因此深受新手的喜愛。為了避免這部分的開銷,一般使用相對布局(RelativeLayout)來重組界面。使用相對布局能夠很方便的將界面由層次多、每層控件少的狹長式樹形機構,轉換成層次少、每層控件多的扁平式樹形結構。從而得到可觀的性能提升。
2. 避免使用layout_weight屬性</h2>
2. 避免使用layout_weight屬性</h2>
layout_weight屬性能夠讓我們根據實際設備的界面大小來動態的調整控件的尺寸。但在Android系統的實現上,對每個指定了layout_weight屬性的布局、控件,系統都會執行兩次的測量計算。這個問題看起來似乎沒什么,但在需要重復解析渲染控件的場合(ListView, GridView)將會由于重復計算變得更加嚴峻。因此要避免使用layout_weight屬性。
那么在平時需要使用到layout_weight屬性的場合,如何將layout_weight屬性優化掉呢?舉個例子:假如需要在水平方向上有兩個按鈕(Button),我們想要讓他們的寬度都是總寬度的一半。
使用layout_weight的做法是這樣的:
<LinearLayout
android:width="match_parent"
android:height="wrap_content"
android:orientation="horizontal"
>
<Button
android:width="0dp"
android:height="wrap_content"
android:weight="1"
/>
<Button
android:width="0dp"
android:height="wrap_content"
android:weight="1"
/>
</LinearLayout> 使用RelativeLayout的做法是這樣的:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerHorizontal="true"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/divider"
android:layout_alignParentLeft="true"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/divider"
android:layout_alignParentRight="true"
/>
</RelativeLayout> 是不是很簡單呢?
3. 使用Lint工具來幫你找到可能的優化點</h2>
3. 使用Lint工具來幫你找到可能的優化點</h2>
Lint工具是Android官方提供的一個優化點掃描工具,它會在每次構建APK包的時候運行。它會根據預設的規則,給出相應的優化建議。舉幾個例子:
- 使用合適的Drawable:一個包含了ImageView和TextView的LinearLayout,用一個復合的Drawable來替代將會更加高效。
- 用merge標簽替代根節點是幀布局(FrameLayout)的布局:如果一個FrameLayout是一個布局文件的根布局,且沒有設置內邊距或背景等屬性,那么可以用merge標簽來代替。這會稍微提升下性能。
- 移除無用的葉節點:如果一個布局沒有子布局、沒有子控件,也沒有設置背景,那么這個布局將會是不可見的,因此也是可以移除的。
- 移除無用的父節點:如果一個布局(1)不是ScrollView、(2)不是根節點、(3)只有一個子節點、(4)沒有設置背景,那么它的子節點可以直接提取到這個父節點的層級上,代替父節點,以便得到一個更加扁平和高效的布局結構。
- 層級過多的布局:層級過身將導致糟糕的性能。盡可能的使用RelativeLayout和GridLayout,讓布局扁平化。布局層次的最大限制是10層。
4. 重用可重用的布局</h2>
4. 重用可重用的布局</h2>
將在多個布局中會用到的部分抽離出來放在一個xml文件中。然后使用include標簽來導入這個布局。抽離出來的布局文件的根節點布局就是你希望它導入其他布局文件之后出現在那個位置的布局,如果不需要這樣一個布局,則可以用merge標簽作為根節點。這兩種有什么不同呢?舉個例子:
<!-- 假設布局文件叫做common_layout.xml -->
<LinearLayout
xmlns:android="
<!-- 假設布局文件叫做common_widget.xml -->
<merge
xmlns:android="
假如導入common_layout.xml,標簽是這樣的:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<include layout="@layout/common_layout" />
</LinearLayout>
導入之后,include標簽區域實際上會被common_layout.xml里從LinearLayout開始的全部布局、控件代替。
那如果導入common_widget.xml呢?導入的標簽跟上面一樣,只不過layout屬性的值換成了@layout/common_widget:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<include layout="@layout/common_widget" />
</LinearLayout>
導入之后,include標簽實際上會被兩個Button代替,也就是使用merge標簽的話,導入的時候merge標簽會被忽略,merge標簽下的控件會被直接放置在文件之中。
還有另一種重用布局的方式:使用fragment。碎片(fragment)是Android 3.0以后引進的一個類似Activity的組件。相比Activity,它更加的輕量,啟動和加載更加的快,而且它可以根據需要加載和切換,并且不需要在AndroidManifest.xml文件中聲明就能使用,但它必須依賴于Activity才能使用。顧名思義,fragment能夠用來組織“小”塊的布局,但除此之外,它還能封裝一系列的邏輯。因此對于可以重用的布局,比如自定義的對話框,可以使用Fragment來組織管理,方便在代碼中重用。
5. 根據需要來加載布局</h2>
有些布局內容(如進度條指示器,某個按鈕點擊后才會出現的額外內容等)并不需要一開始就顯示在界面上,一般在開發中會將其可見性設置為invisible或者gone,在需要時候再設置為visible。雖然一開始這些內容以及沒顯示在界面上了,但實際上在界面初始化的時候,這些內容還是會被加載的。對于這種狀況,使用ViewStub標簽再適合不過了。
首先將需要動態加載的布局抽取出來到一個xml文件中。不過ViewStub不支持merge標簽,這點要注意。假設抽取出來的文件是common_layout.xml,然后將ViewStub里的android:layout屬性指向這個文件。如下示例代碼:
<ViewStub
android:id="@+id/viewstub"
android:inflatedId="@+id/inflated_layout"
android:layout="@layout/common_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
像上面這樣,xml中就定義完畢啦。inflatedId屬性稍后再解釋。那代碼中怎么使用呢?同樣很簡單。有兩種方式:
// 方式一
findViewById(R.id.viewstub).setVisibility(View.VISIBLE);
// 方式二
View view = ((ViewStub) findViewById(R.id.viewstub)).inflate();
通過以上調用,就能把界面給加載出來了。inflatedId屬性的值將會在加載完畢之后賦值給被加載頁面的根節點的id屬性,在這個例子里是common_layout里的最外層LinearLayout。
為什么需要這個屬性呢?事情是這樣的。ViewStub在加載完畢之后就從界面上消失了,這意味著通過ViewStub的id再也無法索引到這個ViewStub了。那么如果我們需要去動態調整加載進來的界面的控件該怎么辦呢?inflatedId這個屬性就是幫我們找到這個布局的關鍵了。如果使用的是方法二來加載頁面,inflate()方法調用將會返回加載出來的布局的View給你,通過這個View也是可以操作加載進來的頁面的。
6. 使用后臺線程,讓ListView流暢滾動</h2>
有經驗的開發者經常建議:不要在主線程進行耗時的操作。但對于初學者來說,到底什么是主線程呢?這個問題我琢磨了很久。主線程其實就是UI線程,這個線程負責處理跟UI相關的操作。界面的繪制、界面的加載、控件的調整等操作都是由主線程來執行的。UI線程之所以被稱作主線程,是因為在絕大多數情況下,我們寫的代碼都會在跑在UI線程里,比如界面的各個生命周期的回調方法(onCreate(),onResume(), onPause()等)。
由于主線程處理跟UI相關的操作,如果主線程處理耗時的操作(文件處理、網絡請求等),那么就會導致主線程無法及時處理其他的UI操作請求,這樣我們在界面上就會明顯的感覺到卡頓。這個問題在處理ListView這種需要重復的繪制列表元素的情況下會變得更加嚴峻。因此需要小心處理ListAdapter里getView()方法里可能存在的耗時操作。
但使用后臺線程就要接觸多線程了,而多線程操作又是眾所周知的坑多。如果不想自己手動去維護自己的線程池或實現自己的多線程機制,那么可以使用AsyncTask類來實現后臺處理任務的需求。這是Android提供的一個非常便捷的方法。
// 使用AsyncTask在后臺加載一張加載很慢的圖片
// 這里的三個泛型參數的意義分別是:
// 1) UI控件、2)進度回調的類型、3)任務執行完畢后的返回類型
new AsyncTask<ViewHolder, Void, Bitmap>() {
// ViewHolder技術是實現復雜ListView的一個優化性能的技術,后面會介紹
private ViewHolder v;
// doInBackground()方法在后臺線程執行
@Override
protected Bitmap doInBackground(ViewHolder... params) {
v = params[0]; // 這里保存一份界面的引用
// 這里假定有一個圖片加載器,getImage()方法是它的一個耗時方法
return mFakeImageLoader.getImage();
}
// onPostExecute()方法在主線程執行,確保所有耗時的操作在doInBackground()方法做完了
@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
if (v.position == position) {
v.progress.setVisibility(View.GONE);
v.icon.setVisibility(View.VISIBLE);
v.icon.setImageBitmap(result);
}
}
}.execute(holder);</pre>
從Android 3.0(API 11)開始,AsyncTask增加了一個方法:AsyncTask.executeOnExecutor()。這個方法會利用處理器的多核特性,進一步提升性能。這個方法的具體效果取決于具體設備的處理器核心數。
7. 使用ViewHolder技術來優化ListView</h2>
前面說到,在ListView這種需要重復的繪制列表元素的情況下,性能問題將變得更加嚴峻。由于內存的原因,ListView只有一個固定數量的View列表來顯示列表的每一項,通過回收不可見的項,重新調整控件來顯示新出現在界面上的項。因此在ListView的滾動過程中,將會頻繁的調用ListAdapter.getView()方法。通過上面的優化建議,我們已經把耗時操作放到后臺線程去執行了,只把必須的UI操作留在主線程。那還能不能再優化呢?答案是肯定的。
如果有經常在網上查找教程,那么應該對ListView的教程里的ViewHolder技術不陌生。這個技術的核心是減少主線程里的執行步驟,以達到優化性能的目的。通過使用ViewHolder,緩存每個View的引用,減少不必要的查找控件操作,以達到優化ListView性能的效果。
首先需要創建一個ViewHolder類來持有View的引用:
static class ViewHolder {
TextView text;
TextView timestamp;
ImageView icon;
ProgressBar progress;
}
然后實例化ViewHolder并為其賦值,再將ViewHolder存儲在view的tag里:
ViewHolder holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.list_item_text);
holder.timestamp = (TextView) convertView.findViewById(R.id.list_item_timestamp);
holder.icon = (ImageView) convertView.findViewById(R.id.list_item_icon);
holder.progress = (ProgressBar) convertView.findViewById(R.id.list_item_progress);
convertView.setTag(holder);
之后就可以從convertView的tag里取出ViewHolder來直接訪問對應的控件進行操作了。
ViewHolder holder = (ViewHolder) convertView.getTag();
holder.text.setText(text);
// ...這里省略對其他控件的操作
8. 為不同尺寸、像素密度的設備提供對應分別率的圖片資源</h2>
這個在學習Android工程目錄結構的時候就應該有所了解。現在一般會提供mdpi、hdpi、xhdpi、xxhdpi四種大小的資源圖片,這樣就能保證你的應用在絕大部分設備上的擁有良好的圖片顯示效果。需要注意的是ldpi這個規格已經廢棄了,不需要再提供這個大小的資源。如果你的程序會運行在比xxhdpi更大更精細尺寸的設備,可以考慮再提供一個xxxhdpi尺寸的資源文件,否則以上四種就足夠了。
9. 為自定義控件的不同狀態提供合適的表現</h2>
如果你實現了自己的控件,那么很有必要創建一個drawable為控件可能處于的狀態提供對應的表現。這些表現是用戶和控件交互能獲得的直觀反饋,設置控件不同狀態下對應的表現的drawable文件大致如下:
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 默認時的背景圖片 -->
<item android:drawable="@drawable/button_default" />
<!-- 沒有焦點時的背景圖片 -->
<item
android:state_focused="false"
android:drawable="@drawable/button_default" />
<!-- 非觸摸模式下獲得焦點并單擊時的背景圖片 -->
<item
android:state_focused="true"
android:state_pressed="true"
android:drawable= "@drawable/button_pressed" />
<!-- 觸摸模式下單擊時的背景圖片 -->
<item
android:state_focused="false"
android:state_pressed="true"
android:drawable="@drawable/button_pressed" />
<!--選中時的圖片背景 -->
<item
android:state_selected="true"
android:drawable="@drawable/button_selected" />
<!--獲得焦點時的圖片背景 -->
<item
android:state_focused="true"
android:drawable="@drawable/button_selected" />
</selector>
10. 使用字體</h2>
Android系統自帶了兩種字體:Droid Sans和Roboto。其中Roboto字體是Android 4.0之后添加的字體,這款字體更加緊湊,在小屏幕設備上也有很好的顯示效果。Android也支持使用TTF格式的自定義的字體,可以將字體放置在Asset文件夾下,也可以通過互聯網下載來使用自定義字體。放在Asset文件夾下的字體由于會打包到安裝包中,因此會稍微增大安裝包體積,假如使用的是中文字體,那就更大了。如果只是程序的某些文字需要用到一些特殊的字體,可以考慮精簡字體庫,或者使用圖片來代替。使用自定義字體的代碼如下:
Typeface font = Typeface.createFromAsset(getAssets(), "my_font.ttf");
((TextView)findViewById(R.layout.MyTextView)).setTypeface(font);
11. 使用點九圖來適應不同大小的控件</h2>
點九圖是一種特殊處理過的png圖片,系統能夠根據需要縮放圖片的大小,去適應不同大小的控件。比如QQ和微信的聊天對話框里的氣泡效果,長文字和短文字的氣泡背景的顯示效果都很好,這就是點九圖能夠實現的效果。
TODO 如何制作點九圖
12. 對于簡單的圖形圖片,盡可能使用向量圖形來繪制。</h2>
Android提供了一種機制,允許通過xml來繪制簡單的圖形。因此對于簡單的圖形圖片,如果可能使用xml來繪制它。由于繪制出來的圖形是基于矢量的,因此這個圖形文件擁有良好的伸縮性,同時也能夠減少內存的占用。圖片資源對內存的消耗很明顯,如果使用圖片過多的話,程序將會使用很多的內存來緩存圖片,而且很容易引起OOM(OutOfMemory)錯誤。
TODO 通過xml繪制基礎的圖形
13. 巧妙使用android:tint屬性來改變圖片顏色</h2>
Android 5.0提供了一個新的屬性android:tint。這個屬性允許我們設置一個疊加顏色來改變圖片的顏色,效果類似于在PhotoShop里設置ColorOverlay屬性。那么對于Android 5.0之前的機器,由于沒有這個屬性,就無法實現同樣的效果了嗎?不是這樣的,雖然xml屬性沒有,但我們可以通過代碼來改變它。
// 這段代碼實現了將support v7包提供的返回按鈕圖片設置為白色的功能
Drawable drawable = getDrawable(R.drawable.abc_ic_ab_back_mtrl_am_alpha);
drawable.setColorFilter(getResources().getColor(R.color.white), PorterDuff.Mode.SRC_IN);
有了這種方式,就可以通過使用一張圖片來變化出不同顏色的效果。再也不需要把為同一張圖片生成不同顏色的版本,在放置在資源文件夾里管理了。
14. 使用樣式(style、dimen、color、string)將布局文件和樣式剝離</h2>
Android里的樣式有點像CSS里的類。樣式允許我們將一組屬性集合起來,并指定一個名字,然后在其他地方通過引用這個名字來使用這組效果。樣式還允許繼承,然后通過重寫父樣式的某些屬性來覆蓋父樣式的屬性。這種理念遵循了一個很古老的編碼原則(DRY: Don't Repeat Yourself)。散落一地的代碼將會為你維護代碼的工作帶來額外的挑戰。
舉個例子,在編寫xml過程中,layout_width和layout_height是兩個必不可少的屬性。我們可以通過定義如下四個樣式來減少我們的代碼書寫量。
<style name="Match">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
</style>
<style name="Wrap">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
</style>
<style
name="MatchHeight"
parent="Match">
<item name="android:layout_width">wrap_content</item>
</style>
<style
name="MatchWidth"
parent="Match">
<item name="android:layout_height">wrap_content</item>
</style></pre>
記住:保持布局xml文件的可維護性的秘訣是吧樣式屬性和定位屬性區分開來。
15. 使用主題</h2>
主題是一系列決定應用程序外觀樣式的集合。系統內置了多重主題,如Android 4.0推出的Holo主題,Android 5.0推出的MaterialDesign。
基于主題做適當的定制化,可以使應用既具備自己的個性,又不會和系統的整體體驗相差太遠。主題在樣式(style)文件里重寫,在AndroidManifest.xml指定,根據需要你可以設定為全應用范圍生效(在application標簽的android:theme屬性指定),也可以設置為某一個Activity內生效(在activity標簽的android:theme屬性指定)。
另外需要注意的是,系統根據資源文件里的標簽(resources下的style、selector、color等)來識別資源文件里定義的資源,而不是根據資源文件的文件名來定位。因此可以放心組織你的資源到不同的資源文件中去。
16. 保持資源的名字結構清晰、意思明確</h2>
-
對于id資源,可采用這樣的命名方式:哪個頁面哪種控件代表含義,如:login_edt_username表示login頁面下一個表示用戶名的EditText。
-
對于圖片(drawable)資源,可以采用這樣的命名方式:類型哪種控件含義,如:ic_ab_edit表示這是一個表示便捷的ActionBar圖標(ic表示icon,ab表示ActionBar)。對于普通圖片標,則可以把中間的哪種控件省略。
-
對于顏色(color)資源,一般用作調色板使用,推薦采用通用的指代顏色的名稱命名。對于主題相關的再額外定義一個文件,指向這些顏色。
17. 對在不同工程中通用的資源,采用通用的命名,以便在不同工程中重用這部分的資源。</h2>
例如通用的顏色資源:
<?xml version="1.0" encoding="utf-8">
<resources>
<color name="primarycolor_dark">#0e5a83</color>
<color name="primarycolor">#0680c3</color>
<color name="primarycolor_light">#489dca</color>
<color name="secondary_light">#999999</color>
<color name="secondary">#565656</color>
<color name="secondary_dark">#4b4b4b</color>
<color name="secondary_extraDark">#231f20</color>
<color name="highlight_one">#35791d</color>
<color name="highlight_two">#ff5151</color>
<color name="main_bg">#ecf0f3</color>
<color name="button_pressed">#036194</color>
<color name="button_default">#0680c3</color>
<color name="button1_pressed">#c04033</color>
<color name="button1_default">#df4534</color>
<color name="button2_pressed">#279030</color>
<color name="button2_default">#37ab41</color>
<color name="button3_pressed">#4f4f4f</color>
<color name="button3_default">#757575</color>
<color name="line_bg">#d6d6d8</color>
</resources>
以及通用的字體字號資源:
<?xml version="1.0" encoding="utf-8">
<resources>
<dimen name="text_mirco">12sp</dimen>
<dimen name="text_ultraMini">13sp</dimen>
<dimen name="text_mini">14sp</dimen>
<dimen name="text_ultraSmall">16sp</dimen>
<dimen name="text_small">17sp</dimen>
<dimen name="text_moderate1">18sp</dimen>
<dimen name="text_moderate2">19sp</dimen>
<dimen name="text_moderate3">21sp</dimen>
<dimen name="text_big">25sp</dimen>
<dimen name="text_extraBig">31sp</dimen>
</resources>
這里為了直觀,顏色資源直接使用了#FFFFFF形式的值。
18. 使用Toolbar、ActionBar或者支持庫(support library)提供的同等控件</h2>
如果你的應用的界面使用了包含ActionBar的設計,那么使用SDK提供的Toolbar或ActionBar來實現。不要重復發明輪子,記住這個編程原則。安卓支持庫v7(support library v7)為適配Android 2.1+的系統提供了支持。如果你使用ActionBar,那么使用ActionBar樣式生成器來方便的定制化它。生成器地址(傳送門)
19. 讓系統幫你完成適配。</h2>
Android系統提供了自動根據設備顯示能力使用最合適的資源的能力。基本所有的資源都支持通過一定規則來動態切換。如可以通過提供一個res/layout-small/文件夾來為小尺寸的設備提供定制化的布局。
20. That's all, But NOT ALL.</h2>
來自:http://sr1.me/way-to-explore/2015/03/25/best-practice-for-android-ui.html