使用Android系統的手機或是平板電腦非常多種,而這些裝置的螢幕尺寸、解析度和密度也都不太相同,因此在開發Android程式時必須考慮到使用者所使用的螢幕規格,並針對這些規格來設計出不同的版面,讓程式可以在各種不同的裝置下順利執行與使用。



基本概念

在開始針對不同螢幕來設計程式之前,最好還是對螢幕有些基本的認識。

螢幕尺寸(Screen Size)

螢幕尺寸是指螢幕的物理大小,可以直接使用量尺工具來量測,一般會測量螢幕的對角線長度。平常我們說的4吋螢幕、5吋螢幕、7吋螢幕,就是在說螢幕對角線的長度。要注意的是這裡的「吋」是指「英吋」,而不是台灣所使用的「台寸」。一英吋(inch)等於2.54公分(cm),而一台寸等於3.03公分(cm),別被混淆囉!

在Android系統上,螢幕尺寸可直接被表示為small、normal、large、xlarge(extra large)、xxlarge(extra extra large)和xxxlarge(extra extra extra large)。small的螢幕寬和高的長度至少為426dp x 320dp,normal的螢幕寬和高的長度至少為470dp x 320dp,large的螢幕寬和高的長度至少為640dp x 480dp,xlarge的螢幕寬和高的長度至少為960dp x 720dp,xxlarge的螢幕寬和高的長度至少為1280dp x 960dp,xxxlarge的螢幕寬和高的長度至少為1920dp x 1440dp。「dp」這個單位等一下將會介紹,如果要將這些值換算成對角線長度的話,那麼可以用以下的方式計算:

實寬 = 寬px / dpi = 寬dp / 160 (單位:英吋)
實高 = 寬px / dpi = 寬dp / 160 (單位:英吋)
對角線長度 = ((實寬*實寬) + (實高*實高))0.5 (單位:英吋)

以上式子出現的「dpi」將在之後介紹,而對角線長度的「0.5」次方表示開根號的意思。

Nexus 7 第二代為例,Nexus 7是7吋的平板電腦,它的寬高長度為960dp x 600dp,螢幕尺寸屬於large(高未滿720dp,尚未到達xlarge),解析度為1920px x 1200px,dpi為320。因此Nexus 7的的實寬為6吋,實高為3.75吋,對角線長度大約是7吋。

當然,也可以利用以上的算法,來算出Android螢幕尺寸大概的範圍:

  • small:3.3吋~3.5吋。
  • normal:3.5吋~5吋。
  • large:5吋~7.5吋。
  • xlarge:7.5吋以上。
  • xxlarge:7.5吋以上。
  • xxlarge:10吋以上。
  • xxxlarge:15吋以上。

螢幕密度(Screen Density)、DPI與像素(Pixel)

螢幕密度是指在一塊螢幕物理面積中的像素數量,但是通常不以面積而是以長度做計算,常用的單位是DPI(Dots Per Inch),也就是一吋的長度要使用幾個點才連接而成。舉個例子,假設DPI為160,若要填滿一吋x一吋(一平方英吋)的區塊,則需要使用到160x160=25600個點,而不是160個點哦!在這裡所說的「點(Dot)」,就是「像素(Pixel)」,像素是螢幕基本的顯示單位,一個像素只能用來表示一種顏色,螢幕正是由多種顏色不同的像素來顯示出畫面的。

在固定的螢幕尺寸下,螢幕密度愈高,畫面看起來就愈精細。在Android系統上,螢幕密度可以直接被表示為ldpi(low)、mdpi(medium)、hdpi(high)、xhdpi(extra high)、xxhdpi(extra extra high)、xxxhdpi(extra extra extra high)。ldpi大約為120dpi,mdpi大約為160dpi,hdpi大約為240dpi,xhdpi大約為320dpi,xxhdpi大約為480dpi,xxxhdpi大約為640dpi。

螢幕方向(Orientation)

從使用者的角度來看,與使用者身體垂直的方向稱為「寬(Width)」,與身體平行的方向則稱為「高(Height)」。我們將寬比高長的螢幕稱為「橫的」,高比寬長的螢幕稱為「直的」。在Android上,「橫的」螢幕畫面表示成「Landscape」,「直的」螢幕畫面則表示成「Portrait」。

解析度(Resolution)

解析度又稱「分辨率」,用來表示組成一塊平面所需要的像素(Pixel)的數量。例如解析度1920x1200,就表示這個平面的寬需要使用1920個像素來組成,高需要使用1200個像素來組成,因此要填滿這塊平面,必須使用1920x1200=2304000個像素。

常見單位

在Android系統中,可以使用多種單位來表示一個長度,以下將區分這些單位的意義與用途。

PX(Pixel)

PX就是表示一個長度所需的像素數量,如果有一個螢幕的解析度是1920x1200,可以直接將其看成1920px x 1200px,表示這個螢幕的寬有1920個像素(或者說是1920個點);高有1200個像素(或者說是1200個點)。如果要算出一個像素的長度的話,可以直接計算DPI的倒數,例如96 dpi,表示一個像素的長度為1/96=0.0104(單位:英吋)。在Android中,為了使程式可以適應各種螢幕,不應該直接使用PX來指定長度。

IN(Inch)

IN就是英吋,不管DPI的大小如何,1in永遠為1英吋,也就是2.54公分,算是一個蠻大的單位。在Android系統中,因為螢幕面板普遍不大,故建議使用下面提到的MM或是PT單位來指定長度。

MM(Millimeter)

MM就是毫米,不管DPI的大小如何,1mm永遠為1毫米。在Android系統中,通常用來進行製作需要固定實際物理長度的畫面,例如一條尺的圖形。若要拿來MM當作文字的長度,可能會太大,難以微調,故建議使用下面提到的PT單位來指定文字長度。

PT(Point)

PT通常用來當作一個文字的長度,不管DPI的大小如何,1pt永遠為1/72英吋(0.01389英吋),也就是說,在DPI為72的情況之下,1pt=1px,如果將DPI提升到96,那麼1pt=1.333px。由此可以看出,固定的pt大小,會隨著DPI改變其使用的像素數量,因此在相同的螢幕尺寸下,即使DPI不同,文字大小也不會有改變。在Android中,如果想要讓文字在各種裝置上都擁有一樣的大小,不會因為原本在5吋螢幕上看文字正常,在其它尺寸螢幕上看文字就變大或變小的話,可以使用PT單位。

DP(Density-independent Pixel)

DP是Android上最常用長度的單位,不管DPI的大小如何,1dp永遠為1/160英吋(0.00625英吋),1/160其實是mdpi的倒數,也就是說,DP是以mdpi作為基準的。在mdpi的情況下,1dp=1px=1/160in;在hdpi的情況下,1dp=240/160px=1.5px=1.5/240in=1/160in。可以看得出來,固定的dp大小,會隨著DPI改變其使用的像素數量,即使DPI不同,實際的長度也不會有改變。DP和PX的轉換公式如下:

px = dp * (dpi / 160)
dp = px * 160 / dpi

在Android中,DP是最小的固定長度單位,非常適合對版面進行細微的調整,如果想固定字體大小的話,也可以使用DP單位。

SP(Scale-independent Pixel)

SP常用來當作文字的長度,基本上SP就是DP(在字型大小100%的時候),但是SP可以再隨著系統的字型大小設定做倍率的縮放。在Android系統中,如果要讓文字大小可以被使用者在系統設定中的字型大小影響,那就使用SP單位指定文字大小吧!

Android App如何支援多種螢幕?

替不同螢幕尺寸和不同螢幕方向指定不同的Layout

通用的Layout XML檔案儲存在Android專案的「res」目錄中的「layout」目錄下,如果要替不同螢幕尺寸指定不同的Layout,可以改用layout-small、layout-normal、layout-large、layout-xlarge目錄來將不同螢幕尺寸的Layout分開來。如果想要直接指定螢幕的最小邊長至少要為多少DP才套用特定的Layout,可以將Layout的目錄名稱寫成「layout-swNdp」,N是DP的大小。

例如「layout-sw600dp」,就是螢幕的最小邊長要至少為600dp才套用這目錄底下的layout。sw的用法將會優先於直接指定螢幕尺寸,也就是說如果「layout-sw1dp」和「layout-small」同時存在,將會先使用「layout-sw1dp」目錄底下的Layout。

如果要指定螢幕為橫向和直向時所使用的Layout,可以將目錄名稱改為「layout-land」或是「layout-port」。當然,也是可以同時指定螢幕尺寸和螢幕方向,例如橫向的Large螢幕可以將目錄名稱改為「layout-large-land」。如果Layout的樣子已經設計成適合在任何螢幕上觀看的話,其實就不太需要再針對不同螢幕尺寸或是方向來指定不同的Layout,這太費工夫了!可以直接使用以下提到的Dimension方式來設計Layout。

替不同螢幕尺寸和不同螢幕方向指定不同的Dimension

在設計Android的Layout的時候,只要是View的位置、大小還是其他有關於尺寸的設定,都應儘量避免直接在Layout的XML或是Java程式中寫入固定的數值。這些數值應該要寫在「res」目錄中的「values」目錄下的「dimens.xml」檔案中。「values」目錄也可以根據螢幕尺寸和螢幕方向來讓Android系統自動選用,同樣地要將目錄名稱修改為「values-large」或是「values-xlarge-land」這樣的格式。

替不同螢幕密度指定不同尺寸的Drawable圖片

Android App中通用的圖片資源,儲存在Android專案的「res」目錄中的「drawable」目錄下。如果要替不同螢幕密度指定不同的Drawable圖片,可以改用drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi目錄來將不同螢幕密度的Drawable圖片分開來,這樣才可以確保Drawable圖片在不同螢幕密度下顯示出來的實際大小都是一樣的。同一張圖片如果要在不同螢幕密度下以一樣的大小顯示,圖片的尺寸(邊長)在ldpi、mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi下的比值是3:4:6:8:12:16。以36x36的圖片在ldpi上顯示出來的結果為基準,在mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi下必須分別使用48x48、72x72、96x96、144x144、192x192的圖片才會看起來一樣大。

以下式子為螢幕密度和圖片尺寸的比例計算方式:

ldpi:mdpi:hdpi:xhdpi = 120:160:240:320:480:640 = 3:4:6:8:12:16

避免設定ActionBar上Menu Item的showAsAction屬性為always

如果將ActionBar上選單(Menu)項目(Item)的showAsAction屬性設定為always,那麼這個項目將會永遠出現在ActionBar上,而無法被收合隱藏。如果Item的名稱過長、數量過多或是Activity的標題文字太長都有可能會因為螢幕不夠寬而導致顯示結果不如預期。

比起always,ifroom更適合用來支援多種螢幕,被設定為ifroom的選單項目只有在ActionBar有足夠空間的時候才會被展開顯示出來。

使用程式計算View的位置與大小

在許多情況下設計師可能會需要自己寫程式來動態產生View,或是動態計算View的位置與大小,這時可能會需要使用螢幕的寬、高尺寸來協助計算,也可能會碰到單位換算的問題,例如:PX轉DP、DP轉PX。在這裡還是要再次提醒設計師,如果要設計出支援多種螢幕的程式,就不能夠直接指定固定的尺寸數值給View,應該要透過「dimens.xml」檔案,來讓Android系統自動去取得適合目前螢幕的尺寸值。

在Android SDK中,可以使用Context的getResources()方法來取得Android App的資源,其中當然也包括Dimension,取得方式如下:

...
float d = getResources().getDimension(dimensionID);

例如:

...
float d = getResources().getDimension(R.dimen.activity_horizontal_margin);

無論在dimens.xml檔案中使用哪種單位來定義尺寸,getDimension()方法所取得的數值都會轉成PX單位,且型態為float。Android SDK的Java程式中,View在絕大多數的時候,都是使用PX單位來表示尺寸,但要注意有個常見的例外,那就是文字的尺寸。以TextView為例,如果使用以下程式來取得TextView,再將取得的值重新設成TextView的文字大小,會發現TextView的文字大小居然改變了!

...
textView.setTextSize(textView.getTextSize());

使用getTextSize方法所得到的尺寸單位是PX,但是直接使用setTextSize方法,傳入的單位居然是SP。還好setTextSize方法還有一個多載用法,可以傳入尺寸的單位。如將以上會改變TextView文字大小的程式改成以下寫法,文字大小就不會變了!

...
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize());

如果要取得螢幕的寬、高尺寸,也可以使用getResources()方法取得App的Resources後,再去使用getDisplayMetrics()方法取得顯示相關的設定值,其中包含了螢幕有多少像素寬、有多少像素高以及螢幕的密度。

以下程式,可以方便地取得螢幕的寬、高,以及進行PX、DP、SP之間的單位換算:

import android.content.Context;
import android.util.DisplayMetrics;

public class UnitUtil {

    public static DisplayMetrics getDisplayMetrics(Context context) {
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        return dm;
    }

    public static float dp2px(Context context, float dpValue) {
        return Math.round(dpValue * getDisplayMetrics(context).density);
    }

    public static float px2dp(Context context, float pxValue) {
        return Math.round(pxValue / getDisplayMetrics(context).density);
    }

    public static float sp2px(Context context, float pxValue) {
        return Math.round(pxValue * getDisplayMetrics(context).scaledDensity);
    }

    public static float px2sp(Context context, float pxValue) {
        return Math.round(pxValue / getDisplayMetrics(context).scaledDensity);
    }

    public static int getScreenWidthPx(Context context) {
        return getDisplayMetrics(context).widthPixels;
    }

    public static int getScreenHeightPx(Context context) {
        return getDisplayMetrics(context).heightPixels;
    }
}