1. 程式人生 > >Android權威官方螢幕適配全攻略

Android權威官方螢幕適配全攻略

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

Android螢幕適配出現的原因

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

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

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

在2012年,OpenSignalMaps(以下簡稱OSM)釋出了第一份Android碎片化報告,統計資料表明,

  1. 2012年,支援Android的裝置共有3997種。
  2. 2013年,支援Android的裝置共有11868種。
  3. 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 進行匹配。

下圖為圖示的各個螢幕密度的對應尺寸

這裡寫圖片描述

解決方案

支援各種螢幕尺寸

使用wrap_content、match_parent、weight

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

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

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

下面是一段示例程式碼

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout android:layout_width="match_parent"
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1"
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content"
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

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

這裡寫圖片描述

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為例說明。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Type here:"/>
    <EditText
        android:id="@+id/entry"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/label"/>
    <Button
        android:id="@+id/ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/entry"
        android:layout_alignParentRight="true"
        android:layout_marginLeft="10dp"
        android:text="OK" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/ok"
        android:layout_alignTop="@id/ok"
        android:text="Cancel" />
</RelativeLayout>

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

在小尺寸螢幕的顯示

這裡寫圖片描述

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

這裡寫圖片描述

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

使用限定符

使用尺寸限定符

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

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

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

res/layout/main.xml,單面板(預設)佈局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</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,單面板(預設)佈局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</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:

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

res/values-sw600dp/layout.xml:

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

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

使用螢幕方向限定符

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

小螢幕,縱向:單面板,帶徽標
小螢幕,橫向:單面板,帶徽標
7 英寸平板電腦,縱向:單面板,帶操作欄
7 英寸平板電腦,橫向:雙面板,寬,帶操作欄
10 英寸平板電腦,縱向:雙面板,窄,帶操作欄
10 英寸平板電腦,橫向:雙面板,寬,帶操作欄
電視,橫向:雙面板,寬,帶操作欄
因此,這些佈局中的每一種都定義在了 res/layout/ 目錄下的某個 XML 檔案中。為了繼續將每個佈局分配給各種螢幕配置,該應用會使用佈局別名將兩者相匹配:

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout android:layout_width="match_parent"
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1"
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content"
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout/twopanes.xml:(雙面板,寬佈局)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="200dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

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

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

res/values/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/onepane_with_bar</item>
    <bool name="has_two_panes">false</bool>
</resources>

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

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

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

<resources>
    <item name="main_layout" type="layout">@layout/onepane</item>
    <bool name="has_two_panes">false</bool>
</resources>

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

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

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

<resources>
    <item name="main_layout" type="layout">@layout/twopanes_narrow</item>
    <bool name="has_two_panes">true</bool>
</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)指定兩個檢視間的間距:

<Button android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/clickme"
    android:layout_marginTop="20dp" />

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

<TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    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份,每一份對應具體的畫素就可以了。

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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;

public class MakeXml {

    private final static String rootPath = "C:\\Users\\Administrator\\Desktop\\layoutroot\\values-{0}x{1}\\";

    private final static float dw = 320f;
    private final static float dh = 480f;

    private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n";
    private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n";

    public static void main(String[] args) {
        makeString(320, 480);
        makeString(480,800);
        makeString(480, 854);
        makeString(540, 960);
        makeString(600, 1024);
        makeString(720, 1184);
        makeString(720, 1196);
        makeString(720, 1280);
        makeString(768, 1024);
        makeString(800, 1280);
        makeString(1080, 1812);
        makeString(1080, 1920);
        makeString(1440, 2560);
    }

    public static void makeString(int w, int h) {

        StringBuffer sb = new StringBuffer();
        sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sb.append("<resources>");
        float cellw = w / dw;
        for (int i = 1; i < 320; i++) {
            sb.append(WTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellw * i) + ""));
        }
        sb.append(WTemplate.replace("{0}", "320").replace("{1}", w + ""));
        sb.append("</resources>");

        StringBuffer sb2 = new StringBuffer();
        sb2.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sb2.append("<resources>");
        float cellh = h / dh;
        for (int i = 1; i < 480; i++) {
            sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellh * i) + ""));
        }
        sb2.append(HTemplate.replace("{0}", "480").replace("{1}", h + ""));
        sb2.append("</resources>");

        String path = rootPath.replace("{0}", h + "").replace("{1}", w + "");
        File rootFile = new File(path);
        if (!rootFile.exists()) {
            rootFile.mkdirs();
        }
        File layxFile = new File(path + "lay_x.xml");
        File layyFile = new File(path + "lay_y.xml");
        try {
            PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
            pw.print(sb.toString());
            pw.close();
            pw = new PrintWriter(new FileOutputStream(layyFile));
            pw.print(sb2.toString());
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }

    public static float change(float a) {
        int temp = (int) (a * 100);
        return temp / 100