Android屏幕適配方案:產品級的解決方案

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寸的設備上,很可能顯示不下。
一句話,總結下,dp能夠讓同一數值在不同的分辨率展示出大致相同的尺寸大小。但是當設備的尺寸差異較大的時候,就無能爲力了。適配的問題還需要我們自己去做,於是我們可能會這麼做:

複製代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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是無法完成的,我們希望控件的尺寸可以按照下列方式編寫: 

複製代碼
1
2
3
4
<Button
     android:text="@string/hello_world"
     android:layout_width="20%w"
     android:layout_height="10%h"/>
利用屏幕的寬和高的比例去定義View的寬和高。 

好了,到此我們可以看到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,我可以怎麼編寫佈局文件呢? 

複製代碼
1
2
3
4
5
6
7
8
9
10
<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 

2) 編寫自動生成文件的程序代碼如下: 

複製代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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; 


按照:

複製代碼
1
java -jar xx.jar width height width,height_width,height

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

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

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

Google已經添加了百分比支持庫,詳情請看:Android 百分比佈局庫(percent-support-lib) 解析與擴展  ok~ 

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