Android屏幕適配全攻略(最權威的官方適配指導)

轉載請註明出處:http://blog.csdn.net/zhaokaiqiang1992

Android的屏幕適配一直以來都在折磨着我們這些開發者,本篇文章以Google的官方文檔爲基礎,全面而深入的講解了Android屏幕適配的原因、重要概念、解決方案及最佳實踐,我相信如果你能認真的學習本文,對於Android的屏幕適配,你將有所收穫!

Android屏幕適配出現的原因

在我們學習如何進行屏幕適配之前,我們需要先了解下爲什麼Android需要進行屏幕適配。

由於Android系統的開放性,任何用戶、開發者、OEM廠商、運營商都可以對Android進行定製,修改成他們想要的樣子。

但是這種“碎片化”到底到達什麼程度呢?

在2012年,OpenSignalMaps(以下簡稱OSM)發佈了第一份Android碎片化報告,統計數據表明,

  • 2012年,支持Android的設備共有3997種。
  • 2013年,支持Android的設備共有11868種。
  • 2014年,支持Android的設備共有18796種。

下面這張圖片所顯示的內容足以充分說明當今Android系統碎片化問題的嚴重性,因爲該圖片中的每一個矩形都代表着一種Android設備。

而隨着支持Android系統的設備(手機、平板、電視、手錶)的增多,設備碎片化、品牌碎片化、系統碎片化、傳感器碎片化和屏幕碎片化的程度也在不斷地加深。而我們今天要探討的,則是對我們開發影響比較大的——屏幕的碎片化。

下面這張圖是Android屏幕尺寸的示意圖,在這張圖裏面,藍色矩形的大小代表不同尺寸,顏色深淺則代表所佔百分比的大小。

而與之相對應的,則是下面這張圖。這張圖顯示了iOS設備所需要進行適配的屏幕尺寸和佔比。

當然,這張圖片只是4,4s,5,5c,5s和平板的尺寸,現在還應該加上新推出的iphone6和plus,但是和Android的屏幕碎片化程度相比而言,還是差的太遠。

詳細的統計數據請到這裏查看

現在你應該很清楚爲什麼要對Android的屏幕進行適配了吧?屏幕尺寸這麼多,爲了讓我們開發的程序能夠比較美觀的顯示在不同尺寸、分辨率、像素密度(這些概念我會在下面詳細講解)的設備上,那就要在開發的過程中進行處理,至於如何去進行處理,這就是我們今天的主題了。

但是在開始進入主題之前,我們再來探討一件事情,那就是Android設備的屏幕尺寸,從幾寸的智能手機,到10寸的平板電腦,再到幾十寸的數字電視,我們應該適配哪些設備呢?

其實這個問題不應該這麼考慮,因爲對於具有相同像素密度的設備來說,像素越高,尺寸就越大,所以我們可以換個思路,將問題從單純的尺寸大小轉換到像素大小和像素密度的角度來。

下圖是2014年初,友盟統計的佔比5%以上的6個主流分辨率,可以看出,佔比最高的是480*800,320*480的設備竟然也佔據了很大比例,但是和半年前的數據相比較,中低分辨率(320*480、480*800)的比例在減少,而中高分辨率的比例則在不斷地增加。雖然每個分辨率所佔的比例在變化,但是總的趨勢沒變,還是這六種,只是分辨率在不斷地提高。

所以說,我們只要儘量適配這幾種分辨率,就可以在大部分的手機上正常運行了。

當然了,這只是手機的適配,對於平板設備(電視也可以看做是平板),我們還需要一些其他的處理。

好了,到目前爲止,我們已經弄清楚了Android開發爲什麼要進行適配,以及我們應該適配哪些對象,接下來,終於進入我們的正題了!

首先,我們先要學習幾個重要的概念。

重要概念

什麼是屏幕尺寸、屏幕分辨率、屏幕像素密度? 
什麼是dp、dip、dpi、sp、px?他們之間的關係是什麼? 
什麼是mdpi、hdpi、xdpi、xxdpi?如何計算和區分?

在下面的內容中我們將介紹這些概念。

屏幕尺寸

屏幕尺寸指屏幕的對角線的長度,單位是英寸,1英寸=2.54釐米

比如常見的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等

屏幕分辨率

屏幕分辨率是指在橫縱向上的像素點數,單位是px,1px=1個像素點。一般以縱向像素*橫向像素,如1960*1080。

屏幕像素密度

屏幕像素密度是指每英寸上的像素點數,單位是dpi,即“dot per inch”的縮寫。屏幕像素密度與屏幕尺寸和屏幕分辨率有關,在單一變化條件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。

dp、dip、dpi、sp、px

px我們應該是比較熟悉的,前面的分辨率就是用的像素爲單位,大多數情況下,比如UI設計、Android原生API都會以px作爲統一的計量單位,像是獲取屏幕寬高等。

dip和dp是一個意思,都是Density Independent Pixels的縮寫,即密度無關像素,上面我們說過,dpi是屏幕像素密度,假如一英寸裏面有160個像素,這個屏幕的像素密度就是160dpi,那麼在這種情況下,dp和px如何換算呢?在Android中,規定以160dpi爲基準,1dip=1px,如果密度是320dpi,則1dip=2px,以此類推。

假如同樣都是畫一條320px的線,在480*800分辨率手機上顯示爲2/3屏幕寬度,在320*480的手機上則佔滿了全屏,如果使用dp爲單位,在這兩種分辨率下,160dp都顯示爲屏幕一般的長度。這也是爲什麼在Android開發中,寫佈局的時候要儘量使用dp而不是px的原因。

而sp,即scale-independent pixels,與dp類似,但是可以根據文字大小首選項進行放縮,是設置字體大小的御用單位。

mdpi、hdpi、xdpi、xxdpi

其實之前還有個ldpi,但是隨着移動設備配置的不斷升級,這個像素密度的設備已經很罕見了,所在現在適配時不需考慮。

mdpi、hdpi、xdpi、xxdpi用來修飾Android中的drawable文件夾及values文件夾,用來區分不同像素密度下的圖片和dimen值。

那麼如何區分呢?Google官方指定按照下列標準進行區分:

名稱 像素密度範圍
mdpi 120dpi~160dpi
hdpi 160dpi~240dpi
xhdpi 240dpi~320dpi
xxhdpi 320dpi~480dpi
xxxhdpi 480dpi~640dpi

在進行開發的時候,我們需要把合適大小的圖片放在合適的文件夾裏面。下面以圖標設計爲例進行介紹。

在設計圖標時,對於五種主流的像素密度(MDPI、HDPI、XHDPI、XXHDPI 和 XXXHDPI)應按照 2:3:4:6:8 的比例進行縮放。例如,一個啓動圖標的尺寸爲48x48 dp,這表示在 MDPI 的屏幕上其實際尺寸應爲 48x48 px,在 HDPI 的屏幕上其實際大小是 MDPI 的 1.5 倍 (72x72 px),在 XDPI 的屏幕上其實際大小是 MDPI 的 2 倍 (96x96 px),依此類推。

雖然 Android 也支持低像素密度 (LDPI) 的屏幕,但無需爲此費神,系統會自動將 HDPI 尺寸的圖標縮小到 1/2 進行匹配。

下圖爲圖標的各個屏幕密度的對應尺寸

屏幕密度 圖標尺寸
mdpi 48x48px
hdpi 72x72px
xhdpi 96x96px
xxhdpi 144x144px
xxxhdpi 192x192px

解決方案

支持各種屏幕尺寸

使用wrap_content、match_parent、weight

要確保佈局的靈活性並適應各種尺寸的屏幕,應使用 “wrap_content” 和 “match_parent” 控制某些視圖組件的寬度和高度。

使用 “wrap_content”,系統就會將視圖的寬度或高度設置成所需的最小尺寸以適應視圖中的內容,而 “match_parent”(在低於 API 級別 8 的級別中稱爲 “fill_parent”)則會展開組件以匹配其父視圖的尺寸。

如果使用 “wrap_content” 和 “match_parent” 尺寸值而不是硬編碼的尺寸,視圖就會相應地僅使用自身所需的空間或展開以填滿可用空間。此方法可讓佈局正確適應各種屏幕尺寸和屏幕方向。

下面是一段示例代碼

<code
 class="hljs xml has-numbering" style="display: block; padding: 0px; 
background-color: transparent; color: inherit; box-sizing: border-box; 
font-family: 'Source Code Pro', monospace;font-size:undefined; 
white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 
0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; 
word-wrap: normal; background-position: initial initial; 
background-repeat: initial initial;"><span class="hljs-tag" 
style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span
 class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 
136);">LinearLayout</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">xmlns:android</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"http://schemas.android.com/apk/res/android"</span>
    <span class="hljs-attribute" style="box-sizing: border-box; 
color: rgb(102, 0, 102);">android:orientation</span>=<span 
class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 
0);">"vertical"</span>
    <span class="hljs-attribute" style="box-sizing: border-box; 
color: rgb(102, 0, 102);">android:layout_width</span>=<span 
class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 
0);">"match_parent"</span>
    <span class="hljs-attribute" style="box-sizing: border-box; 
color: rgb(102, 0, 102);">android:layout_height</span>=<span
 class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 
0);">"match_parent"</span>></span>
    <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"><<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">LinearLayout</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">android:layout_width</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"match_parent"</span>
                  <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:id</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@+id/linearLayout1"</span>  
                  <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:gravity</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"center"</span>
                  <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_height</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"50dp"</span>></span>
        <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"><<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">ImageView</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">android:id</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@+id/imageView1"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_height</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"wrap_content"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_width</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"wrap_content"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:src</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@drawable/logo"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:paddingRight</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"30dp"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_gravity</span>=<span 
class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 
0);">"left"</span>
                   <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_weight</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"0"</span> /></span>
        <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"><<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">View</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">android:layout_height</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"wrap_content"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:id</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@+id/view1"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_width</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"wrap_content"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_weight</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"1"</span> /></span>
        <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"><<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">Button</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">android:id</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@+id/categorybutton"</span>
                <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:background</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@drawable/button_bg"</span>
                <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_height</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"match_parent"</span>
                <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_weight</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"0"</span>
                <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_width</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"120dp"</span>
                <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 102);">style</span>=<span 
class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@style/CategoryButtonStyle"</span>/></span>
    <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"></<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">LinearLayout</span>></span>

    <span class="hljs-tag" style="color: rgb(0, 102, 102); 
box-sizing: border-box;"><<span class="hljs-title" 
style="box-sizing: border-box; color: rgb(0, 0, 
136);">fragment</span> <span class="hljs-attribute" 
style="box-sizing: border-box; color: rgb(102, 0, 
102);">android:id</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"@+id/headlines"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_height</span>=<span class="hljs-value"
 style="box-sizing: border-box; color: rgb(0, 136, 
0);">"fill_parent"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:name</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"com.example.android.newsreader.HeadlinesFragment"</span>
              <span class="hljs-attribute" style="box-sizing: 
border-box; color: rgb(102, 0, 
102);">android:layout_width</span>=<span class="hljs-value" 
style="box-sizing: border-box; color: rgb(0, 136, 
0);">"match_parent"</span> /></span>
<span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: 
border-box;"></<span class="hljs-title" style="box-sizing: 
border-box; color: rgb(0, 0, 
136);">LinearLayout</span>></span></code><ul 
class="pre-numbering" style="box-sizing: border-box; position: absolute;
 width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px;
 margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; 
border-right-style: solid; border-right-color: rgb(221, 221, 221); 
list-style: none; text-align: right;"><li style="box-sizing: 
border-box; padding: 0px 5px;">1</li><li style="box-sizing: 
border-box; padding: 0px 5px;">2</li><li style="box-sizing: 
border-box; padding: 0px 5px;">3</li><li style="box-sizing: 
border-box; padding: 0px 5px;">4</li><li style="box-sizing: 
border-box; padding: 0px 5px;">5</li><li style="box-sizing: 
border-box; padding: 0px 5px;">6</li><li style="box-sizing: 
border-box; padding: 0px 5px;">7</li><li style="box-sizing: 
border-box; padding: 0px 5px;">8</li><li style="box-sizing: 
border-box; padding: 0px 5px;">9</li><li style="box-sizing: 
border-box; padding: 0px 5px;">10</li><li style="box-sizing:
 border-box; padding: 0px 5px;">11</li><li 
style="box-sizing: border-box; padding: 0px 5px;">12</li><li
 style="box-sizing: border-box; padding: 0px 
5px;">13</li><li style="box-sizing: border-box; padding: 0px
 5px;">14</li><li style="box-sizing: border-box; padding: 
0px 5px;">15</li><li style="box-sizing: border-box; padding:
 0px 5px;">16</li><li style="box-sizing: border-box; 
padding: 0px 5px;">17</li><li style="box-sizing: border-box;
 padding: 0px 5px;">18</li><li style="box-sizing: 
border-box; padding: 0px 5px;">19</li><li style="box-sizing:
 border-box; padding: 0px 5px;">20</li><li 
style="box-sizing: border-box; padding: 0px 5px;">21</li><li
 style="box-sizing: border-box; padding: 0px 
5px;">22</li><li style="box-sizing: border-box; padding: 0px
 5px;">23</li><li style="box-sizing: border-box; padding: 
0px 5px;">24</li><li style="box-sizing: border-box; padding:
 0px 5px;">25</li><li style="box-sizing: border-box; 
padding: 0px 5px;">26</li><li style="box-sizing: border-box;
 padding: 0px 5px;">27</li><li style="box-sizing: 
border-box; padding: 0px 5px;">28</li><li style="box-sizing:
 border-box; padding: 0px 5px;">29</li><li 
style="box-sizing: border-box; padding: 0px 5px;">30</li><li
 style="box-sizing: border-box; padding: 0px 
5px;">31</li><li style="box-sizing: border-box; padding: 0px
 5px;">32</li></ul>

下圖是在橫縱屏切換的時候的顯示效果,我們可以看到這樣可以很好的適配屏幕尺寸的變化。

weight是線性佈局的一個獨特的屬性,我們可以使用這個屬性來按照比例對界面進行分配,完成一些特殊的需求。

但是,我們對於這個屬性的計算應該如何理解呢?

首先看下面的例子,我們在佈局中這樣設置我們的界面

我們在佈局裏面設置爲線性佈局,橫向排列,然後放置兩個寬度爲0dp的按鈕,分別設置weight爲1和2,在效果圖中,我們可以看到兩個按鈕按照1:2的寬度比例正常排列了,這也是我們經常使用到的場景,這是時候很好理解,Button1的寬度就是1/(1+2) = 1/3,Button2的寬度則是2/(1+2) = 2/3,我們可以很清楚的明白這種情景下的佔比如何計算。

但是假如我們的寬度不是0dp(wrap_content和0dp的效果相同),則是match_parent呢?

下面是設置爲match_parent的效果

我們可以看到,在這種情況下,佔比和上面正好相反,這是怎麼回事呢?說到這裏,我們就不得不提一下weight的計算方法了。

android:layout_weight的真實含義是:如果View設置了該屬性並且有效,那麼該 View的寬度等於原有寬度(android:layout_width)加上剩餘空間的佔比。

從這個角度我們來解釋一下上面的現象。在上面的代碼中,我們設置每個Button的寬度都是match_parent,假設屏幕寬度爲L,那麼每個Button的寬度也應該都爲L,剩餘寬度就等於L-(L+L)= -L。

Button1的weight=1,剩餘寬度佔比爲1/(1+2)= 1/3,所以最終寬度爲L+1/3*(-L)=2/3L,Button2的計算類似,最終寬度爲L+2/3(-L)=1/3L。

這是在水平方向上的,那麼在垂直方向上也是這樣嗎?

下面是測試代碼和效果

如果是垂直方向,那麼我們應該改變的是layout_height的屬性,下面是0dp的顯示效果

下面是match_parent的顯示效果,結論和水平是完全一樣的

雖然說我們演示了match_parent的顯示效果,並說明了原因,但是在真正用的時候,我們都是設置某一個屬性爲0dp,然後按照權重計算所佔百分比。

使用相對佈局,禁用絕對佈局

在開發中,我們大部分時候使用的都是線性佈局、相對佈局和幀佈局,絕對佈局由於適配性極差,所以極少使用。

由於各種佈局的特點不一樣,所以不能說哪個佈局好用,到底應該使用什麼佈局只能根據實際需求來確定。我們可以使用 LinearLayout 的嵌套實例並結合 “wrap_content” 和 “match_parent”,以便構建相當複雜的佈局。不過,我們無法通過 LinearLayout 精確控制子視圖的特殊關係;系統會將 LinearLayout 中的視圖直接並排列出。

如果我們需要將子視圖排列出各種效果而不是一條直線,通常更合適的解決方法是使用 RelativeLayout,這樣就可以根據各組件之間的特殊關係指定佈局了。例如,我們可以將某個子視圖對齊到屏幕左側,同時將另一個視圖對齊到屏幕右側。

下面的代碼以官方Demo爲例說明。

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.     <TextView  
  6.         android:id="@+id/label"  
  7.         android:layout_width="match_parent"  
  8.         android:layout_height="wrap_content"  
  9.         android:text="Type here:"/>  
  10.     <EditText  
  11.         android:id="@+id/entry"  
  12.         android:layout_width="match_parent"  
  13.         android:layout_height="wrap_content"  
  14.         android:layout_below="@id/label"/>  
  15.     <Button  
  16.         android:id="@+id/ok"  
  17.         android:layout_width="wrap_content"  
  18.         android:layout_height="wrap_content"  
  19.         android:layout_below="@id/entry"  
  20.         android:layout_alignParentRight="true"  
  21.         android:layout_marginLeft="10dp"  
  22.         android:text="OK" />  
  23.     <Button  
  24.         android:layout_width="wrap_content"  
  25.         android:layout_height="wrap_content"  
  26.         android:layout_toLeftOf="@id/ok"  
  27.         android:layout_alignTop="@id/ok"  
  28.         android:text="Cancel" />  
  29. </RelativeLayout>  

在上面的代碼中我們使用了相對佈局,並且使用alignXXX等屬性指定了子控件的位置,下面是這種佈局方式在應對屏幕變化時的表現

在小尺寸屏幕的顯示

在平板的大尺寸上的顯示效果

雖然控件的大小由於屏幕尺寸的增加而發生了改變,但是我們可以看到,由於使用了相對佈局,所以控件之前的位置關係並沒有發生什麼變化,這說明我們的適配成功了。

使用限定符

使用尺寸限定符

上面所提到的靈活佈局或者是相對佈局,可以爲我們帶來的優勢就只有這麼多了。雖然這些佈局可以拉伸組件內外的空間以適應各種屏幕,但它們不一定能爲每種屏幕都提供最佳的用戶體驗。因此,我們的應用不僅僅只實施靈活佈局,還應該應針對各種屏幕配置提供一些備用佈局。

如何做到這一點呢?我們可以通過使用配置限定符,在運行時根據當前的設備配置自動選擇合適的資源了,例如根據各種屏幕尺寸選擇不同的佈局。

很多應用會在較大的屏幕上實施“雙面板”模式,即在一個面板上顯示項目列表,而在另一面板上顯示對應內容。平板電腦和電視的屏幕已經大到可以同時容納這兩個面板了,但手機屏幕就需要分別顯示。因此,我們可以使用以下文件以便實施這些佈局:

res/layout/main.xml,單面板(默認)佈局:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:orientation="vertical"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.   
  6.     <fragment android:id="@+id/headlines"  
  7.               android:layout_height="fill_parent"  
  8.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  9.               android:layout_width="match_parent" />  
  10. </LinearLayout>  

res/layout-large/main.xml,雙面板佈局:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="horizontal">  
  5.     <fragment android:id="@+id/headlines"  
  6.               android:layout_height="fill_parent"  
  7.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  8.               android:layout_width="400dp"  
  9.               android:layout_marginRight="10dp"/>  
  10.     <fragment android:id="@+id/article"  
  11.               android:layout_height="fill_parent"  
  12.               android:name="com.example.android.newsreader.ArticleFragment"  
  13.               android:layout_width="fill_parent" />  
  14. </LinearLayout>  

請注意第二種佈局名稱目錄中的 large 限定符。系統會在屬於較大屏幕(例如 7 英寸或更大的平板電腦)的設備上選擇此佈局。系統會在較小的屏幕上選擇其他佈局(無限定符)。

使用最小寬度限定符

在版本低於 3.2 的 Android 設備上,開發人員遇到的問題之一是“較大”屏幕的尺寸範圍,該問題會影響戴爾 Streak、早期的 Galaxy Tab 以及大部分 7 英寸平板電腦。即使這些設備的屏幕屬於“較大”的尺寸,但很多應用可能會針對此類別中的各種設備(例如 5 英寸和 7 英寸的設備)顯示不同的佈局。這就是 Android 3.2 版在引入其他限定符的同時引入“最小寬度”限定符的原因。

最小寬度限定符可讓您通過指定某個最小寬度(以 dp 爲單位)來定位屏幕。例如,標準 7 英寸平板電腦的最小寬度爲 600 dp,因此如果您要在此類屏幕上的用戶界面中使用雙面板(但在較小的屏幕上只顯示列表),您可以使用上文中所述的單面板和雙面板這兩種佈局,但您應使用 sw600dp 指明雙面板佈局僅適用於最小寬度爲 600 dp 的屏幕,而不是使用 large 尺寸限定符。

res/layout/main.xml,單面板(默認)佈局:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:orientation="vertical"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.   
  6.     <fragment android:id="@+id/headlines"  
  7.               android:layout_height="fill_parent"  
  8.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  9.               android:layout_width="match_parent" />  
  10. </LinearLayout>  

res/layout-sw600dp/main.xml,雙面板佈局:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="horizontal">  
  5.     <fragment android:id="@+id/headlines"  
  6.               android:layout_height="fill_parent"  
  7.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  8.               android:layout_width="400dp"  
  9.               android:layout_marginRight="10dp"/>  
  10.     <fragment android:id="@+id/article"  
  11.               android:layout_height="fill_parent"  
  12.               android:name="com.example.android.newsreader.ArticleFragment"  
  13.               android:layout_width="fill_parent" />  
  14. </LinearLayout>  

也就是說,對於最小寬度大於等於 600 dp 的設備,系統會選擇 layout-sw600dp/main.xml(雙面板)佈局,否則系統就會選擇 layout/main.xml(單面板)佈局。

但 Android 版本低於 3.2 的設備不支持此技術,原因是這些設備無法將 sw600dp 識別爲尺寸限定符,因此我們仍需使用 large 限定符。這樣一來,就會有一個名稱爲 res/layout-large/main.xml 的文件(與 res/layout-sw600dp/main.xml 一樣)。但是沒有太大關係,我們將馬上學習如何避免此類佈局文件出現的重複。

使用佈局別名

最小寬度限定符僅適用於 Android 3.2 及更高版本。因此,如果我們仍需使用與較低版本兼容的概括尺寸範圍(小、正常、大和特大)。例如,如果要將用戶界面設計成在手機上顯示單面板,但在 7 英寸平板電腦、電視和其他較大的設備上顯示多面板,那麼我們就需要提供以下文件:

  • res/layout/main.xml: 單面板佈局
  • res/layout-large: 多面板佈局
  • res/layout-sw600dp: 多面板佈局

後兩個文件是相同的,因爲其中一個用於和 Android 3.2 設備匹配,而另一個則是爲使用較低版本 Android 的平板電腦和電視準備的。

要避免平板電腦和電視的文件出現重複(以及由此帶來的維護問題),您可以使用別名文件。例如,您可以定義以下佈局:

  • res/layout/main.xml,單面板佈局
  • res/layout/main_twopanes.xml,雙面板佈局

然後添加這兩個文件:

res/values-large/layout.xml:

  1. <resources>  
  2.     <item name="main" type="layout">@layout/main_twopanes</item>  
  3. </resources>  

res/values-sw600dp/layout.xml:

  1. <resources>  
  2.     <item name="main" type="layout">@layout/main_twopanes</item>  
  3. </resources>  

後兩個文件的內容相同,但它們並未實際定義佈局。它們只是將 main 設置成了 main_twopanes 的別名。由於這些文件包含 large 和 sw600dp 選擇器,因此無論 Android 版本如何,系統都會將這些文件應用到平板電腦和電視上(版本低於 3.2 的平板電腦和電視會匹配 large,版本高於 3.2 的平板電腦和電視則會匹配 sw600dp)。

使用屏幕方向限定符

某些佈局會同時支持橫向模式和縱向模式,但我們可以通過調整優化其中大部分佈局的效果。在新聞閱讀器示例應用中,每種屏幕尺寸和屏幕方向下的佈局行爲方式如下所示:

  • 小屏幕,縱向:單面板,帶徽標
  • 小屏幕,橫向:單面板,帶徽標
  • 7 英寸平板電腦,縱向:單面板,帶操作欄
  • 7 英寸平板電腦,橫向:雙面板,寬,帶操作欄
  • 10 英寸平板電腦,縱向:雙面板,窄,帶操作欄
  • 10 英寸平板電腦,橫向:雙面板,寬,帶操作欄
  • 電視,橫向:雙面板,寬,帶操作欄

因此,這些佈局中的每一種都定義在了 res/layout/ 目錄下的某個 XML 文件中。爲了繼續將每個佈局分配給各種屏幕配置,該應用會使用佈局別名將兩者相匹配:

res/layout/onepane.xml:(單面板)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:orientation="vertical"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.   
  6.     <fragment android:id="@+id/headlines"  
  7.               android:layout_height="fill_parent"  
  8.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  9.               android:layout_width="match_parent" />  
  10. </LinearLayout>  

res/layout/onepane_with_bar.xml:(單面板帶操作欄)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:orientation="vertical"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.     <LinearLayout android:layout_width="match_parent"  
  6.                   android:id="@+id/linearLayout1"    
  7.                   android:gravity="center"  
  8.                   android:layout_height="50dp">  
  9.         <ImageView android:id="@+id/imageView1"  
  10.                    android:layout_height="wrap_content"  
  11.                    android:layout_width="wrap_content"  
  12.                    android:src="@drawable/logo"  
  13.                    android:paddingRight="30dp"  
  14.                    android:layout_gravity="left"  
  15.                    android:layout_weight="0" />  
  16.         <View android:layout_height="wrap_content"  
  17.               android:id="@+id/view1"  
  18.               android:layout_width="wrap_content"  
  19.               android:layout_weight="1" />  
  20.         <Button android:id="@+id/categorybutton"  
  21.                 android:background="@drawable/button_bg"  
  22.                 android:layout_height="match_parent"  
  23.                 android:layout_weight="0"  
  24.                 android:layout_width="120dp"  
  25.                 style="@style/CategoryButtonStyle"/>  
  26.     </LinearLayout>  
  27.   
  28.     <fragment android:id="@+id/headlines"  
  29.               android:layout_height="fill_parent"  
  30.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  31.               android:layout_width="match_parent" />  
  32. </LinearLayout>  
res/layout/twopanes.xml:(雙面板,寬佈局)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="horizontal">  
  5.     <fragment android:id="@+id/headlines"  
  6.               android:layout_height="fill_parent"  
  7.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  8.               android:layout_width="400dp"  
  9.               android:layout_marginRight="10dp"/>  
  10.     <fragment android:id="@+id/article"  
  11.               android:layout_height="fill_parent"  
  12.               android:name="com.example.android.newsreader.ArticleFragment"  
  13.               android:layout_width="fill_parent" />  
  14. </LinearLayout>  


res/layout/twopanes_narrow.xml:(雙面板,窄佈局)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="horizontal">  
  5.     <fragment android:id="@+id/headlines"  
  6.               android:layout_height="fill_parent"  
  7.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  8.               android:layout_width="200dp"  
  9.               android:layout_marginRight="10dp"/>  
  10.     <fragment android:id="@+id/article"  
  11.               android:layout_height="fill_parent"  
  12.               android:name="com.example.android.newsreader.ArticleFragment"  
  13.               android:layout_width="fill_parent" />  
  14. </LinearLayout>  


既然我們已定義了所有可能的佈局,那就只需使用配置限定符將正確的佈局映射到各種配置即可。

現在只需使用佈局別名技術即可做到這一點:

res/values/layouts.xml:

  1. <resources>  
  2.     <item name="main_layout" type="layout">@layout/onepane_with_bar</item>  
  3.     <bool name="has_two_panes">false</bool>  
  4. </resources>  

res/values-sw600dp-land/layouts.xml:

  1. <resources>  
  2.     <item name="main_layout" type="layout">@layout/twopanes</item>  
  3.     <bool name="has_two_panes">true</bool>  
  4. </resources>  

res/values-sw600dp-port/layouts.xml:

  1. <resources>  
  2.     <item name="main_layout" type="layout">@layout/onepane</item>  
  3.     <bool name="has_two_panes">false</bool>  
  4. </resources>  

res/values-large-land/layouts.xml:
  1. <resources>  
  2.     <item name="main_layout" type="layout">@layout/twopanes</item>  
  3.     <bool name="has_two_panes">true</bool>  
  4. </resources>  

res/values-large-port/layouts.xml:
  1. <resources>  
  2.     <item name="main_layout" type="layout">@layout/twopanes_narrow</item>  
  3.     <bool name="has_two_panes">true</bool>  
  4. </resources>  


使用自動拉伸位圖

支持各種屏幕尺寸通常意味着您的圖片資源還必須能適應各種尺寸。例如,無論要應用到什麼形狀的按鈕上,按鈕背景都必須能適應。

如果在可以更改尺寸的組件上使用了簡單的圖片,您很快就會發現顯示效果多少有些不太理想,因爲系統會在運行時平均地拉伸或收縮您的圖片。解決方法爲使用自動拉伸位圖,這是一種格式特殊的 PNG 文件,其中會指明可以拉伸以及不可以拉伸的區域。

.9的製作,實際上就是在原圖片上添加1px的邊界,然後按照我們的需求,把對應的位置設置成黑色線,系統就會根據我們的實際需求進行拉伸。

下圖是對.9圖的四邊的含義的解釋,左上邊代表拉伸區域,右下邊代表padding box,就是間隔區域,在下面,我們給出一個例子,方便大家理解。

先看下面兩張圖,我們理解一下這四條線的含義。

上圖和下圖的區別,就在於右下邊的黑線不一樣,具體的效果的區別,看右邊的效果圖。上圖效果圖中深藍色的區域,代表內容區域,我們可以看到是在正中央的,這是因爲我們在右下邊的是兩個點,這兩個點距離上下左右四個方向的距離就是padding的距離,所以深藍色內容區域在圖片正中央,我們再看下圖,由於右下邊的黑線是圖片長度,所以就沒有padding,從效果圖上的表現就是深藍色區域和圖片一樣大,因此,我們可以利用右下邊來控制內容與背景圖邊緣的padding。

如果你還不明白,那麼我們看下面的效果圖,我們分別以圖一和圖二作爲背景圖,下面是效果圖。

我們可以看到,使用wrap_content屬性設置長寬,圖一比圖二的效果大一圈,這是爲什麼呢?還記得我上面說的padding嗎?

這就是padding的效果提現,怎麼證明呢?我們再看下面一張圖,給圖一添加padding=0,這樣背景圖設置的padding效果就沒了,是不是兩個一樣大了?

ok,我想你應該明白右下邊的黑線的含義了,下面我們再看一下左上邊的效果。

下面我們只設置了左上邊線,效果圖如下

上面的線沒有包住圖標,下面的線正好包住了圖標,從右邊的效果圖應該可以看出差別,黑線所在的區域就是拉伸區域,上圖黑線所在的全是純色,所以圖標不變形,下面的拉伸區域包裹了圖標,所以在拉伸的時候就會對圖標進行拉伸,但是這樣就會導致圖標變形。注意到下面紅線區域了嘛?這是系統提示我們的,因爲這樣拉伸,不符合要求,所以會提示一下。

支持各種屏幕密度

使用非密度制約像素

由於各種屏幕的像素密度都有所不同,因此相同數量的像素在不同設備上的實際大小也有所差異,這樣使用像素定義佈局尺寸就會產生問題。因此,請務必使用 dp 或 sp 單位指定尺寸。dp 是一種非密度制約像素,其尺寸與 160 dpi 像素的實際尺寸相同。sp 也是一種基本單位,但它可根據用戶的偏好文字大小進行調整(即尺度獨立性像素),因此我們應將該測量單位用於定義文字大小。

例如,請使用 dp(而非 px)指定兩個視圖間的間距:

  1. <Button android:layout_width="wrap_content"  
  2.     android:layout_height="wrap_content"  
  3.     android:text="@string/clickme"  
  4.     android:layout_marginTop="20dp" />  

請務必使用 sp 指定文字大小:

  1. <TextView android:layout_width="match_parent"  
  2.     android:layout_height="wrap_content"  
  3.     android:textSize="20sp" />  


除了介紹這些最基礎的知識之外,我們下面再來討論一下另外一個問題。

經過上面的介紹,我們都清楚,爲了能夠規避不同像素密度的陷阱,Google推薦使用dp來代替px作爲控件長度的度量單位,但是我們來看下面的一個場景。

假如我們以Nexus5作爲書寫代碼時查看效果的測試機型,Nexus5的總寬度爲360dp,我們現在需要在水平方向上放置兩個按鈕,一個是150dp左對齊,另外一個是200dp右對齊,中間留有10dp間隔,那麼在Nexus5上面的顯示效果就是下面這樣

但是如果在Nexus S或者是Nexus One運行呢?下面是運行結果

可以看到,兩個按鈕發生了重疊。

我們都已經用了dp了,爲什麼會出現這種情況呢?

你聽我慢慢道來。

雖然說dp可以去除不同像素密度的問題,使得1dp在不同像素密度上面的顯示效果相同,但是還是由於Android屏幕設備的多樣性,如果使用dp來作爲度量單位,並不是所有的屏幕的寬度都是相同的dp長度,比如說,Nexus S和Nexus One屬於hdpi,屏幕寬度是320dp,而Nexus 5屬於xxhdpi,屏幕寬度是360dp,Galaxy Nexus屬於xhdpi,屏幕寬度是384dp,Nexus 6 屬於xxxhdpi,屏幕寬度是410dp。所以說,光Google自己一家的產品就已經有這麼多的標準,而且屏幕寬度和像素密度沒有任何關聯關係,即使我們使用dp,在320dp寬度的設備和410dp的設備上,還是會有90dp的差別。當然,我們儘量使用match_parent和wrap_content,儘可能少的用dp來指定控件的具體長寬,再結合上權重,大部分的情況我們都是可以做到適配的。

但是除了這個方法,我們還有沒有其他的更徹底的解決方案呢?

我們換另外一個思路來思考這個問題。

下面的方案來自Android Day Day Up 一羣的【blue-深圳】,謝謝他的分享精神

因爲分辨率不一樣,所以不能用px;因爲屏幕寬度不一樣,所以要小心的用dp,那麼我們可不可以用另外一種方法來統一單位,不管分辨率是多大,屏幕寬度用一個固定的值的單位來統計呢?

答案是:當然可以。

我們假設手機屏幕的寬度都是320某單位,那麼我們將一個屏幕寬度的總像素數平均分成320份,每一份對應具體的像素就可以了。

具體如何來實現呢?我們看下面的代碼

  1. import java.io.File;  
  2. import java.io.FileNotFoundException;  
  3. import java.io.FileOutputStream;  
  4. import java.io.PrintWriter;  
  5.   
  6. public class MakeXml {  
  7.   
  8.     private final static String rootPath = "C:\\Users\\Administrator\\Desktop\\layoutroot\\values-{0}x{1}\\";  
  9.   
  10.     private final static float dw = 320f;  
  11.     private final static float dh = 480f;  
  12.   
  13.     private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n";  
  14.     private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n";  
  15.   
  16.     public static void main(String[] args) {  
  17.         makeString(320480);  
  18.         makeString(480,800);  
  19.         makeString(480854);  
  20.         makeString(540960);  
  21.         makeString(6001024);  
  22.         makeString(7201184);  
  23.         makeString(7201196);  
  24.         makeString(7201280);  
  25.         makeString(7681024);  
  26.         makeString(8001280);  
  27.         makeString(10801812);  
  28.         makeString(10801920);  
  29.         makeString(14402560);  
  30.     }  
  31.   
  32.     public static void makeString(int w, int h) {  
  33.   
  34.         StringBuffer sb = new StringBuffer();  
  35.         sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");  
  36.         sb.append("<resources>");  
  37.         float cellw = w / dw;  
  38.         for (int i = 1; i < 320; i++) {  
  39.             sb.append(WTemplate.replace("{0}", i + "").replace("{1}",  
  40.                     change(cellw * i) + ""));  
  41.         }  
  42.         sb.append(WTemplate.replace("{0}""320").replace("{1}", w + ""));  
  43.         sb.append("</resources>");  
  44.   
  45.         StringBuffer sb2 = new StringBuffer();  
  46.         sb2.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");  
  47.         sb2.append("<resources>");  
  48.         float cellh = h / dh;  
  49.         for (int i = 1; i < 480; i++) {  
  50.             sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",  
  51.                     change(cellh * i) + ""));  
  52.         }  
  53.         sb2.append(HTemplate.replace("{0}""480").replace("{1}", h + ""));  
  54.         sb2.append("</resources>");  
  55.   
  56.         String path = rootPath.replace("{0}", h + "").replace("{1}", w + "");  
  57.         File rootFile = new File(path);  
  58.         if (!rootFile.exists()) {  
  59.             rootFile.mkdirs();  
  60.         }  
  61.         File layxFile = new File(path + "lay_x.xml");  
  62.         File layyFile = new File(path + "lay_y.xml");  
  63.         try {  
  64.             PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));  
  65.             pw.print(sb.toString());  
  66.             pw.close();  
  67.             pw = new PrintWriter(new FileOutputStream(layyFile));  
  68.             pw.print(sb2.toString());  
  69.             pw.close();  
  70.         } catch (FileNotFoundException e) {  
  71.             e.printStackTrace();  
  72.         }  
  73.   
  74.     }  
  75.   
  76.     public static float change(float a) {  
  77.         int temp = (int) (a * 100);  
  78.         return temp / 100f;  
  79.     }  
  80. }  

代碼應該很好懂,我們將一個屏幕寬度分爲320份,高度480份,然後按照實際像素對每一個單位進行復制,放在對應values-widthxheight文件夾下面的lax.xml和lay.xml裏面,這樣就可以統一所有你想要的分辨率的單位了,下面是生成的一個320*480分辨率的文件,因爲寬高分割之後總分數和像素數相同,所以x1就是1px,以此類推

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources><dimen name="x1">1.0px</dimen>  
  3. <dimen name="x2">2.0px</dimen>  
  4. <dimen name="x3">3.0px</dimen>  
  5. <dimen name="x4">4.0px</dimen>  
  6. <dimen name="x5">5.0px</dimen>  
  7. <dimen name="x6">6.0px</dimen>  
  8. <dimen name="x7">7.0px</dimen>  
  9. <dimen name="x8">8.0px</dimen>  
  10. <dimen name="x9">9.0px</dimen>  
  11. <dimen name="x10">10.0px</dimen>  
  12. ...省略好多行  
  13. <dimen name="x300">300.0px</dimen>  
  14. <dimen name="x301">301.0px</dimen>  
  15. <dimen name="x302">302.0px</dimen>  
  16. <dimen name="x303">303.0px</dimen>  
  17. <dimen name="x304">304.0px</dimen>  
  18. <dimen name="x305">305.0px</dimen>  
  19. <dimen name="x306">306.0px</dimen>  
  20. <dimen name="x307">307.0px</dimen>  
  21. <dimen name="x308">308.0px</dimen>  
  22. <dimen name="x309">309.0px</dimen>  
  23. <dimen name="x310">310.0px</dimen>  
  24. <dimen name="x311">311.0px</dimen>  
  25. <dimen name="x312">312.0px</dimen>  
  26. <dimen name="x313">313.0px</dimen>  
  27. <dimen name="x314">314.0px</dimen>  
  28. <dimen name="x315">315.0px</dimen>  
  29. <dimen name="x316">316.0px</dimen>  
  30. <dimen name="x317">317.0px</dimen>  
  31. <dimen name="x318">318.0px</dimen>  
  32. <dimen name="x319">319.0px</dimen>  
  33. <dimen name="x320">320px</dimen>  
  34. </resources>  


那麼1080*1960分辨率下是什麼樣子呢?我們可以看下,由於1080和320是3.37倍的關係,所以x1=3.37px

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources><dimen name="x1">3.37px</dimen>  
  3. <dimen name="x2">6.75px</dimen>  
  4. <dimen name="x3">10.12px</dimen>  
  5. <dimen name="x4">13.5px</dimen>  
  6. <dimen name="x5">16.87px</dimen>  
  7. <dimen name="x6">20.25px</dimen>  
  8. <dimen name="x7">23.62px</dimen>  
  9. <dimen name="x8">27.0px</dimen>  
  10. <dimen name="x9">30.37px</dimen>  
  11. <dimen name="x10">33.75px</dimen>  
  12. ...省略好多行  
  13. <dimen name="x300">1012.5px</dimen>  
  14. <dimen name="x301">1015.87px</dimen>  
  15. <dimen name="x302">1019.25px</dimen>  
  16. <dimen name="x303">1022.62px</dimen>  
  17. <dimen name="x304">1026.0px</dimen>  
  18. <dimen name="x305">1029.37px</dimen>  
  19. <dimen name="x306">1032.75px</dimen>  
  20. <dimen name="x307">1036.12px</dimen>  
  21. <dimen name="x308">1039.5px</dimen>  
  22. <dimen name="x309">1042.87px</dimen>  
  23. <dimen name="x310">1046.25px</dimen>  
  24. <dimen name="x311">1049.62px</dimen>  
  25. <dimen name="x312">1053.0px</dimen>  
  26. <dimen name="x313">1056.37px</dimen>  
  27. <dimen name="x314">1059.75px</dimen>  
  28. <dimen name="x315">1063.12px</dimen>  
  29. <dimen name="x316">1066.5px</dimen>  
  30. <dimen name="x317">1069.87px</dimen>  
  31. <dimen name="x318">1073.25px</dimen>  
  32. <dimen name="x319">1076.62px</dimen>  
  33. <dimen name="x320">1080px</dimen>  
  34. </resources>  

無論在什麼分辨率下,x320都是代表屏幕寬度,y480都是代表屏幕高度。

那麼,我們應該如何使用呢?

首先,我們要把生成的所有values文件夾放到res目錄下,當設計師把UI高清設計圖給你之後,你就可以根據設計圖上的尺寸,以某一個分辨率的機型爲基礎,找到對應像素數的單位,然後設置給控件即可。

下圖還是兩個Button,不同的是,我們把單位換成了我們在values文件夾下dimen的值,這樣在你指定的分辨率下,不管寬度是320dp、360dp,還是410dp,就都可以完全適配了。

但是,還是有個問題,爲什麼下面的三個沒有適配呢?

這是因爲由於在生成的values文件夾裏,沒有對應的分辨率,其實一開始是報錯的,因爲默認的values沒有對應dimen,所以我只能在默認values裏面也創建對應文件,但是裏面的數據卻不好處理,因爲不知道分辨率,我只好默認爲x1=1dp保證儘量兼容。這也是這個解決方案的幾個弊端,對於沒有生成對應分辨率文件的手機,會使用默認values文件夾,如果默認文件夾沒有,就會出現問題。

所以說,這個方案雖然是一勞永逸,但是由於實際上還是使用的px作爲長度的度量單位,所以多少和google的要求有所背離,不好說以後會不會出現什麼不可預測的問題。其次,如果要使用這個方案,你必須儘可能多的包含所有的分辨率,因爲這個是使用這個方案的基礎,如果有分辨率缺少,會造成顯示效果很差,甚至出錯的風險,而這又勢必會增加軟件包的大小和維護的難度,所以大家自己斟酌,擇優使用。

更多信息可參考鴻洋的新文章Android 屏幕適配方案

提供備用位圖

由於 Android 可在具有各種屏幕密度的設備上運行,因此我們提供的位圖資源應始終可以滿足各類普遍密度範圍的要求:低密度、中等密度、高密度以及超高密度。這將有助於我們的圖片在所有屏幕密度上都能得到出色的質量和效果。

要生成這些圖片,我們應先提取矢量格式的原始資源,然後根據以下尺寸範圍針對各密度生成相應的圖片。

  • xhdpi:2.0
  • hdpi:1.5
  • mdpi:1.0(最低要求)
  • ldpi:0.75

也就是說,如果我們爲 xhdpi 設備生成了 200x200 px尺寸的圖片,就應該使用同一資源爲 hdpi、mdpi 和 ldpi 設備分別生成 150x150、100x100 和 75x75 尺寸的圖片。

然後,將生成的圖片文件放在 res/ 下的相應子目錄中(mdpi、hdpi、xhdpi、xxhdpi),系統就會根據運行您應用的設備的屏幕密度自動選擇合適的圖片。

這樣一來,只要我們引用 @drawable/id,系統都能根據相應屏幕的 dpi 選取合適的位圖。

還記得我們上面提到的圖標設計尺寸嗎?和這個其實是一個意思。

但是還有個問題需要注意下,如果是.9圖或者是不需要多個分辨率的圖片,就放在drawable文件夾即可,對應分辨率的圖片要正確的放在合適的文件夾,否則會造成圖片拉伸等問題。

實施自適應用戶界面流程

前面我們介紹過,如何根據設備特點顯示恰當的佈局,但是這樣做,會使得用戶界面流程可能會有所不同。例如,如果應用處於雙面板模式下,點擊左側面板上的項即可直接在右側面板上顯示相關內容;而如果該應用處於單面板模式下,點擊相關的內容應該跳轉到另外一個Activity進行後續的處理。所以我們應該按照下面的流程,一步步的完成自適應界面的實現。

確定當前佈局

由於每種佈局的實施都會稍有不同,因此我們需要先確定當前向用戶顯示的佈局。例如,我們可以先了解用戶所處的是“單面板”模式還是“雙面板”模式。要做到這一點,可以通過查詢指定視圖是否存在以及是否已顯示出來。

  1. public class NewsReaderActivity extends FragmentActivity {  
  2.     boolean mIsDualPane;  
  3.   
  4.     @Override  
  5.     public void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.main_layout);  
  8.   
  9.         View articleView = findViewById(R.id.article);  
  10.         mIsDualPane = articleView != null &&  
  11.                         articleView.getVisibility() == View.VISIBLE;  
  12.     }  
  13. }  

請注意,這段代碼用於查詢“報道”面板是否可用,與針對具體佈局的硬編碼查詢相比,這段代碼的靈活性要大得多。

再舉一個適應各種組件的存在情況的方法示例:在對這些組件執行操作前先查看它們是否可用。例如,新聞閱讀器示例應用中有一個用於打開菜單的按鈕,但只有在版本低於 3.0 的 Android 上運行該應用時,這個按鈕纔會存在,因爲 API 級別 11 或更高級別中的 ActionBar 已取代了該按鈕的功能。因此,您可以使用以下代碼爲此按鈕添加事件偵聽器:

  1. Button catButton = (Button) findViewById(R.id.categorybutton);  
  2. OnClickListener listener = /* create your listener here */;  
  3. if (catButton != null) {  
  4.     catButton.setOnClickListener(listener);  
  5. }  

根據當前佈局做出響應

有些操作可能會因當前的具體佈局而產生不同的結果。例如,在新聞閱讀器示例中,如果用戶界面處於雙面板模式下,那麼點擊標題列表中的標題就會在右側面板中打開相應報道;但如果用戶界面處於單面板模式下,那麼上述操作就會啓動一個獨立活動:

  1. @Override  
  2. public void onHeadlineSelected(int index) {  
  3.     mArtIndex = index;  
  4.     if (mIsDualPane) {  
  5.         /* display article on the right pane */  
  6.         mArticleFragment.displayArticle(mCurrentCat.getArticle(index));  
  7.     } else {  
  8.         /* start a separate activity */  
  9.         Intent intent = new Intent(this, ArticleActivity.class);  
  10.         intent.putExtra("catIndex", mCatIndex);  
  11.         intent.putExtra("artIndex", index);  
  12.         startActivity(intent);  
  13.     }  
  14. }  

同樣,如果該應用處於雙面板模式下,就應設置帶導航標籤的操作欄;但如果該應用處於單面板模式下,就應使用下拉菜單設置導航欄。因此我們的代碼還應確定哪種情況比較合適:

  1. final String CATEGORIES[] = { "熱門報道""政治""經濟""Technology" };  
  2.   
  3. public void onCreate(Bundle savedInstanceState) {  
  4.     ....  
  5.     if (mIsDualPane) {  
  6.         /* use tabs for navigation */  
  7.         actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);  
  8.         int i;  
  9.         for (i = 0; i < CATEGORIES.length; i++) {  
  10.             actionBar.addTab(actionBar.newTab().setText(  
  11.                 CATEGORIES[i]).setTabListener(handler));  
  12.         }  
  13.         actionBar.setSelectedNavigationItem(selTab);  
  14.     }  
  15.     else {  
  16.         /* use list navigation (spinner) */  
  17.         actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);  
  18.         SpinnerAdapter adap = new ArrayAdapter(this,  
  19.                 R.layout.headline_item, CATEGORIES);  
  20.         actionBar.setListNavigationCallbacks(adap, handler);  
  21.     }  
  22. }  

重複使用其他活動中的片段

多屏幕設計中的重複模式是指,對於某些屏幕配置,已實施界面的一部分會用作面板;但對於其他配置,這部分就會以獨立活動的形式存在。例如,在新聞閱讀器示例中,對於較大的屏幕,新聞報道文本會顯示在右側面板中;但對於較小的屏幕,這些文本就會以獨立活動的形式存在。

在類似情況下,通常可以在多個活動中重複使用相同的 Fragment 子類以避免代碼重複。例如,在雙面板佈局中使用了 ArticleFragment:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="horizontal">  
  5.     <fragment android:id="@+id/headlines"  
  6.               android:layout_height="fill_parent"  
  7.               android:name="com.example.android.newsreader.HeadlinesFragment"  
  8.               android:layout_width="400dp"  
  9.               android:layout_marginRight="10dp"/>  
  10.     <fragment android:id="@+id/article"  
  11.               android:layout_height="fill_parent"  
  12.               android:name="com.example.android.newsreader.ArticleFragment"  
  13.               android:layout_width="fill_parent" />  
  14. </LinearLayout>  


然後又在小屏幕的Activity佈局中重複使用了它 :

  1. ArticleFragment frag = new ArticleFragment();  
  2. getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();  

當然,這與在 XML 佈局中聲明片段的效果是一樣的,但在這種情況下卻沒必要使用 XML 佈局,因爲報道片段是此活動中的唯一組件。

請務必在設計片段時注意,不要針對具體活動創建強耦合。要做到這一點,通常可以定義一個接口,該接口概括了相關片段與其主活動交互所需的全部方式,然後讓主活動實施該界面:

例如,新聞閱讀器應用的 HeadlinesFragment 會精確執行以下代碼:

  1. public class HeadlinesFragment extends ListFragment {  
  2.     ...  
  3.     OnHeadlineSelectedListener mHeadlineSelectedListener = null;  
  4.   
  5.     /* Must be implemented by host activity */  
  6.     public interface OnHeadlineSelectedListener {  
  7.         public void onHeadlineSelected(int index);  
  8.     }  
  9.     ...  
  10.   
  11.     public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {  
  12.         mHeadlineSelectedListener = listener;  
  13.     }  
  14. }  


然後,如果用戶選擇某個標題,相關片段就會通知由主活動指定的偵聽器(而不是通知某個硬編碼的具體活動):

  1. public class HeadlinesFragment extends ListFragment {  
  2.     ...  
  3.     @Override  
  4.     public void onItemClick(AdapterView<?> parent,  
  5.                             View view, int position, long id) {  
  6.         if (null != mHeadlineSelectedListener) {  
  7.             mHeadlineSelectedListener.onHeadlineSelected(position);  
  8.         }  
  9.     }  
  10.     ...  
  11. }  


除此之外,我們還可以使用第三方框架,比如說使用“訂閱-發佈”模式的EventBus來更多的優化組件之間的通信,減少耦合。

處理屏幕配置變化

如果我們使用獨立Activity實施界面的獨立部分,那麼請注意,我們可能需要對特定配置變化(例如屏幕方向的變化)做出響應,以便保持界面的一致性。

例如,在運行 Android 3.0 或更高版本的標準 7 英寸平板電腦上,如果新聞閱讀器示例應用運行在縱向模式下,就會在使用獨立活動顯示新聞報道;但如果該應用運行在橫向模式下,就會使用雙面板佈局。

也就是說,如果用戶處於縱向模式下且屏幕上顯示的是用於閱讀報道的活動,那麼就需要在檢測到屏幕方向變化(變成橫向模式)後執行相應操作,即停止上述活動並返回主活動,以便在雙面板佈局中顯示相關內容:

  1. public class ArticleActivity extends FragmentActivity {  
  2.     int mCatIndex, mArtIndex;  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         mCatIndex = getIntent().getExtras().getInt("catIndex"0);  
  8.         mArtIndex = getIntent().getExtras().getInt("artIndex"0);  
  9.   
  10.         // If should be in two-pane mode, finish to return to main activity  
  11.         if (getResources().getBoolean(R.bool.has_two_panes)) {  
  12.             finish();  
  13.             return;  
  14.         }  
  15.         ...  
  16. }  


通過上面幾個步驟,我們就完全可以建立一個可以根據用戶界面配置進行自適應的App了。

最佳實踐

關於高清設計圖尺寸

Google官方給出的高清設計圖尺寸有兩種方案,一種是以mdpi設計,然後對應放大得到更高分辨率的圖片,另外一種則是以高分辨率作爲設計大小,然後按照倍數對應縮小到小分辨率的圖片。

根據經驗,我更推薦第二種方法,因爲小分辨率在生成高分辨率圖片的時候,會出現像素丟失,我不知道是不是有方法可以阻止這種情況發生。

而分辨率可以以1280*720或者是1960*1080作爲主要分辨率進行設計。

ImageView的ScaleType屬性

設置不同的ScaleType會得到不同的顯示效果,一般情況下,設置爲centerCrop能獲得較好的適配效果。

動態設置

有一些情況下,我們需要動態的設置控件大小或者是位置,比如說popwindow的顯示位置和偏移量等,這個時候我們可以動態的獲取當前的屏幕屬性,然後設置合適的數值

  1. public class ScreenSizeUtil {  
  2.   
  3.     public static int getScreenWidth(Activity activity) {  
  4.         return activity.getWindowManager().getDefaultDisplay().getWidth();  
  5.     }  
  6.   
  7.     public static int getScreenHeight(Activity activity) {  
  8.         return activity.getWindowManager().getDefaultDisplay().getHeight();  
  9.     }  
  10.   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章