1、概述

大家在Android開發時,肯定會覺得螢幕適配是個尤其痛苦的事,各種螢幕尺寸適配起來蛋疼無比。如果我們換個角度我們看下這個問題,不知道大家有沒有了解過web前端開發,或者說大家對於網頁都不陌生吧,其實適配的問題在web頁面的設計中理論上也存在,為什麼這麼說呢?電腦的顯示器的解析度、包括手機解析度,我敢說解析度的種類遠超過Android裝置的解析度,那麼有一個很奇怪的現象:

為什麼Web頁面設計人員從來沒有說過,尼瑪適配好麻煩?

那麼,到底是什麼原因,讓網頁的設計可以在千差萬別的解析度的解析度中依舊能給使用者一個優質的體驗呢?帶著這個疑惑,我問了下媳婦(前端人員),媳婦睜大眼睛問我:什麼叫適配?fc,尼瑪,看來的確沒有這類問題。後來再我仔細的追問後,她告訴我,噢,這個尺寸呀,我都是設定為20%的~~追根到底,其實就是一個原因,網頁提供了百分比計算大小。

同樣的,大家拿到UI給的設計圖以後,是不是抱怨過尼瑪你標識的都是px,我專案裡面用dp,這什麼玩意,和UI人員解釋,UI妹妹也不理解。那麼本例同樣可以解決Android工程師和UI妹妹間的矛盾~UI給出一個固定尺寸的設計稿,然後你在編寫佈局的時候不用思考,無腦照抄上面標識的畫素值,就能達到完美適配,理想豐不豐滿~~。

然而,Android對於不同的螢幕給出的適配方案是dp,那麼dp與百分比的差距到底在哪裡?

2、dp vs 百分比

  • dp

我們首先看下dp的定義:

Density-independent pixel (dp)獨立畫素密度。標準是160dip.即1dp對應1個pixel,計算公式如:px = dp * (dpi / 160),螢幕密度越大,1dp對應 的畫素點越多。 
上面的公式中有個dpi,dpi為DPI是Dots Per Inch(每英寸所列印的點數),也就是當裝置的dpi為160的時候1px=1dp;

好了,上述這些概念記不記得住沒關係,只要記住一點dp是與畫素無關的,在實際使用中1dp大約等於1/160inch。

那麼dp究竟解決了適配上的什麼問題?可以看出1dp = 1/160inch;那麼它至少能解決一個問題,就是你在佈局檔案寫某個View的寬和高為160dp*160dp,這個View在任何解析度的螢幕中,顯示的尺寸大小是大約是一致的(可能不精確),大概是 1 inch * 1 inch。

但是,這樣並不能夠解決所有的適配問題:

  • 呈現效果仍舊會有差異,僅僅是相近而已
  • 當裝置的物理尺寸存在差異的時候,dp就顯得無能為力了。為4.3寸螢幕準備的UI,執行在5.0寸的螢幕上,很可能在右側和下側存在大量的空白。而5.0寸的UI執行到4.3寸的裝置上,很可能顯示不下。

以上兩點,來自參考連結1

一句話,總結下,dp能夠讓同一數值在不同的解析度展示出大致相同的尺寸大小。但是當裝置的尺寸差異較大的時候,就無能為力了。適配的問題還需要我們自己去做,於是我們可能會這麼做:

<?xml version="1.0" encoding="utf-8"?>  
<resources>  
    <!-- values-hdpi 480X800 -->  
    <dimen name="imagewidth">120dip</dimen>      
</resources>  

<resources>  
    <!-- values-hdpi-1280x800 -->  
    <dimen name="imagewidth">220dip</dimen>      
</resources>  


<?xml version="1.0" encoding="utf-8"?>  
<resources>  
    <!-- values-hdpi  480X320 -->  
    <dimen name="imagewidth">80dip</dimen>      
</resources> 


上述程式碼片段來自網路,也就是說,我們為了優質的使用者體驗,依然需要去針對不同的dpi設定,編寫多套數值檔案。

可以看出,dp並沒有能解決適配問題。下面看百分比。

  • 百分比 
    這個概念不用說了,web中支援控制元件的寬度可以去參考父控制元件的寬度去設定百分比,最外層控制元件的寬度參考螢幕尺寸設定百分比,那麼其實中Android裝置中,只需要支援控制元件能夠參考螢幕的百分比去計算寬高就足夠了。

比如,我現在以下幾個需求:

  • 對於圖片展示的Banner,為了起到該有的效果,我希望在任何手機上顯示的高度為螢幕高度的1/4
  • 我的首頁分上下兩欄,我希望每個欄目的螢幕高度為11/24,中間間隔為1/12
  • slidingmenu的寬度為螢幕寬度的80%

當然了這僅僅是從一個大的層面上來說,其實小範圍佈局,可能百分比將會更加有用。

那麼現在不支援百分比,實現上述的需求,可能需要1、程式碼去動態計算(很多人直接pass了,太麻煩);2、利用weight(weight必須依賴Linearlayout,而且並不能適用於任何場景)

再比如:我的某個浮動按鈕的高度和寬度希望是螢幕高度的1/12,我的某個Button的寬度希望是螢幕寬度的1/3。

上述的所有的需求,利用dp是無法完成的,我們希望控制元件的尺寸可以按照下列方式編寫:

   <Button
        android:text="@string/hello_world"
        android:layout_width="20%w"
        android:layout_height="10%h"/>

好了,到此我們可以看到dp與百分比的區別,而百分比能夠更好的解決我們的適配問題。

  • some 適配tips

我們再來看看一些適配的tips

  1. 多用match_parent
  2. 多用weight
  3. 自定義view解決

其實上述3點tip,歸根結底還是利用百分比,match_parent相當於100%參考父控制元件;weight即按比例分配;自定義view無非是因為裡面多數尺寸是按照百分比計算的;

通過這些tips,我們更加的看出如果能在Android中引入百分比的機制,將能解決大多數的適配問題,下面我們就來看看如何能夠讓Android支援百分比的概念。

3、百分比的引入

1、引入

其實我們的解決方案,就是在專案中針對你所需要適配的手機螢幕的解析度各自簡歷一個資料夾。

如下圖:

然後我們根據一個基準,為基準的意思就是:

比如480*320的解析度為基準

  • 寬度為320,將任何解析度的寬度分為320份,取值為x1-x320
  • 高度為480,將任何解析度的高度分為480份,取值為y1-y480

例如對於800*480的寬度480:

可以看到x1 = 480 / 基準 = 480 / 320 = 1.5 ;

其他解析度類似~~ 
你可能會問,這麼多檔案,難道我們要手算,然後自己編寫?不要怕,下文會說。

那麼,你可能有個疑問,這麼寫有什麼好處呢?

假設我現在需要在螢幕中心有個按鈕,寬度和高度為我們螢幕寬度的1/2,我可以怎麼編寫佈局檔案呢?

<FrameLayout >

    <Button
        android:layout_gravity="center"
        android:gravity="center"
        android:text="@string/hello_world"
        android:layout_width="@dimen/x160"
        android:layout_height="@dimen/x160"/>

</FrameLayout>

可以看到我們的寬度和高度定義為x160,其實就是寬度的50%; 

那麼效果圖:

可以看到不論在什麼解析度的機型,我們的按鈕的寬和高始終是螢幕寬度的一半。

  • 對於設計圖

假設現在的UI的設計圖是按照480*320設計的,且上面的寬和高的標識都是px的值,你可以直接將px轉化為x[1-320],y[1-480],這樣寫出的佈局基本就可以全解析度適配了。

你可能會問:設計師設計圖的解析度不固定怎麼辦?下文會說~

  • 對於上文提出的幾個dp做不到的

你可以通過在引入百分比後,自己試試~~

好了,有個最主要的問題,我們沒有說,就是解析度這麼多,尼瑪難道我們要自己計算,然後手寫?

2、自動生成工具

好了,其實這樣的資料夾手寫也可以,按照你們需要支援的解析度,然後編寫一套,以後一直使用。

當然了,作為程式設計師的我們,怎麼能做這麼low的工作,肯定要程式來實現:

那麼實現需要以下步驟:

  1. 分析需要的支援的解析度

對於主流的解析度我已經整合到了我們的程式中,當然對於特殊的,你可以通過引數指定。關於螢幕解析度資訊,可以通過該網站查詢:http://screensiz.es/phone

  1. 編寫自動生成檔案的程式

程式碼如下

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

/**
 * Created by zhy on 15/5/3.
 */
public class GenerateValueFiles {

    private int baseW;
    private int baseH;

    private String dirStr = "./res";

    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";

    /**
     * {0}-HEIGHT
     */
    private final static String VALUE_TEMPLATE = "values-{0}x{1}";

    private static final String SUPPORT_DIMESION = "320,480;480,800;480,854;540,960;600,1024;720,1184;720,1196;720,1280;768,1024;800,1280;1080,1812;1080,1920;1440,2560;";

    private String supportStr = SUPPORT_DIMESION;

    public GenerateValueFiles(int baseX, int baseY, String supportStr) {
        this.baseW = baseX;
        this.baseH = baseY;

        if (!this.supportStr.contains(baseX + "," + baseY)) {
            this.supportStr += baseX + "," + baseY + ";";
        }

        this.supportStr += validateInput(supportStr);

        System.out.println(supportStr);

        File dir = new File(dirStr);
        if (!dir.exists()) {
            dir.mkdir();

        }
        System.out.println(dir.getAbsoluteFile());

    }

    /**
     * @param supportStr
     *            w,h_...w,h;
     * @return
     */
    private String validateInput(String supportStr) {
        StringBuffer sb = new StringBuffer();
        String[] vals = supportStr.split("_");
        int w = -1;
        int h = -1;
        String[] wh;
        for (String val : vals) {
            try {
                if (val == null || val.trim().length() == 0)
                    continue;

                wh = val.split(",");
                w = Integer.parseInt(wh[0]);
                h = Integer.parseInt(wh[1]);
            } catch (Exception e) {
                System.out.println("skip invalidate params : w,h = " + val);
                continue;
            }
            sb.append(w + "," + h + ";");
        }

        return sb.toString();
    }

    public void generate() {
        String[] vals = supportStr.split(";");
        for (String val : vals) {
            String[] wh = val.split(",");
            generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1]));
        }

    }

    private void generateXmlFile(int w, int h) {

        StringBuffer sbForWidth = new StringBuffer();
        sbForWidth.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sbForWidth.append("<resources>");
        float cellw = w * 1.0f / baseW;

        System.out.println("width : " + w + "," + baseW + "," + cellw);
        for (int i = 1; i < baseW; i++) {
            sbForWidth.append(WTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellw * i) + ""));
        }
        sbForWidth.append(WTemplate.replace("{0}", baseW + "").replace("{1}",
                w + ""));
        sbForWidth.append("</resources>");

        StringBuffer sbForHeight = new StringBuffer();
        sbForHeight.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sbForHeight.append("<resources>");
        float cellh = h *1.0f/ baseH;
        System.out.println("height : "+ h + "," + baseH + "," + cellh);
        for (int i = 1; i < baseH; i++) {
            sbForHeight.append(HTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellh * i) + ""));
        }
        sbForHeight.append(HTemplate.replace("{0}", baseH + "").replace("{1}",
                h + ""));
        sbForHeight.append("</resources>");

        File fileDir = new File(dirStr + File.separator
                + VALUE_TEMPLATE.replace("{0}", h + "")//
                        .replace("{1}", w + ""));
        fileDir.mkdir();

        File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml");
        File layyFile = new File(fileDir.getAbsolutePath(), "lay_y.xml");
        try {
            PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
            pw.print(sbForWidth.toString());
            pw.close();
            pw = new PrintWriter(new FileOutputStream(layyFile));
            pw.print(sbForHeight.toString());
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

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

    public static void main(String[] args) {
        int baseW = 320;
        int baseH = 400;
        String addition = "";
        try {
            if (args.length >= 3) {
                baseW = Integer.parseInt(args[0]);
                baseH = Integer.parseInt(args[1]);
                addition = args[2];
            } else if (args.length >= 2) {
                baseW = Integer.parseInt(args[0]);
                baseH = Integer.parseInt(args[1]);
            } else if (args.length >= 1) {
                addition = args[0];
            }
        } catch (NumberFormatException e) {

            System.err
                    .println("right input params : java -jar xxx.jar width height w,h_w,h_..._w,h;");
            e.printStackTrace();
            System.exit(-1);
        }

        new GenerateValueFiles(baseW, baseH, addition).generate();
    }

}


同時我提供了jar包,預設情況下,雙擊即可生成,使用說明:

下載地址見文末,內建了常用的解析度,預設基準為480*320,當然對於特殊需求,通過命令列指定即可:

例如:基準 1280 * 800 ,額外支援尺寸:1152 * 735;4500 * 3200;

按照

java -jar xx.jar width height width,height_width,height

上述格式即可。

到此,我們通過編寫一個工具,根據某基準尺寸,生成所有需要適配解析度的values檔案,做到了編寫佈局檔案時,可以參考螢幕的解析度;在UI給出的設計圖,可以快速的按照其標識的px單位進行編寫佈局。基本解決了適配的問題。

本方案思想已經有公司投入使用,個人認為還是很不錯的,如果大家有更好的方案來解決螢幕適配的問題,歡迎留言探討或者直接貼出好文連結,大家可以將自己的經驗進行分享,這樣才能壯大我們的隊伍~~。

注:本方案思想來自Android Day Day Up 一群的【blue-深圳】,經其同意編寫此文,上述程式也很大程度上借鑑了其分享的原始碼。在此標識感謝,預祝其創業成功!

===>後期更新

ok~