Android基礎之屏幕適配
衆所周知,Android機型尺寸五花八門,於是屏幕適配被成爲我們Android開發很重要的一部分,Android屏幕適配的時候大家或多或少都會遇到各種問題,所以這也是面試一家新公司,面試官基本會問的一個問題,因爲在我的角度看,不同公司不同項目它的屏幕適配的方法都可能不一樣的。下面讓我來分享我在網上看的各種屏幕適配資料再加上自己的實踐得出的心得和經驗。
基本概念
- 屏幕尺寸
屏幕尺寸指的是屏幕的對角線的長度,單位是英寸,1英寸=2.54釐米,比如說iPhone7是4.7寸,iPhone7Plus是5.5寸。(勿吐槽我,因爲Android機太多了,說某個品牌大家不一定能馬上反應過來)
- 屏幕分辨率
屏幕分辨率是指在橫縱方向上的像素點數,單位是px,1px = 1個像素點。一般的說法是縱向像素*橫向像素,例如,1920*1080。大家可以理解成縱向有1920個點,橫向1080個點。
- 屏幕像素密度
屏幕像素密度匙指每英寸上的像素點數,單位是dpi,是dpi。在單一變化條件下,屏幕尺寸越小,分辨率越高,像素密度越大,反之越小。
- dp、dip、sp、px、dpi
px這個是像素的單位,px均爲整數。
dip、dp是同一個意思,是Density Independent Pixels的縮寫,即密度無關像素。是Android特有的單位,規定以160dpi爲基準,即1dp對應1個pixel,計算公式如:px = dp * (dpi / 160),屏幕密度越大,1dp對應 的像素點越多。
sp我們大多數用在設置字體的大小上,sp儘量不要用小數,可能在適配的過程中導致精度丟失。
- .9圖
上邊和左邊是設置拉伸,最好對稱和不要影響圖裏面的不能拉伸的元素,通常點一個點即可,需要的話要對稱。下邊和右邊是內邊距,要拉長。(這裏簡單的描述一下,需要深入瞭解的同學可以自己去實踐喲。)
Android Drawable
- mdpi、hdpi、xdpi、xxdpi
mdpi、hdpi、xdpi、xxdpi用來修飾Android中drawable文件夾及values文件夾,用來區分不同像素密度下的圖片和dimen值。還有的是有項目會使用values-1920*1080來區分。
那麼Google官方的標準是這樣的:
名稱 | 像素密度範圍 | 比例 |
---|---|---|
mdpi | 120dpi~160dpi | 2 |
hdpi | 160dpi~240dpi | 3 |
xhdpi | 240dpi~320dpi | 4 |
xxhdpi | 320dpi~480dpi | 6 |
xxxhdpi | 480dpi~640dpi | 8 |
問題來了,我們是不是要在每一種dpi都出一套圖片資源,按照上述是一種處理方法,但是是不是覺得這樣的工作量很大,不說我們,你的美工MM會不會幫你弄都是一個問題,還有你各個目錄都放一套圖片資源,到時你打包的apk就變得很大。那麼有沒有什麼好的方法既能保證屏幕適配,又可以最小佔用設計資源,同時最好又只使用一套dpi的圖片資源呢?
好啦,假設我們只放一套圖片資源在hdpi的文件夾下,然後我們的測試機的dpi是跑xhdpi文件夾的,系統發現xhdpi沒有圖片資源,然後就跑去了我們放圖片資源的hdpi的文件夾讀取圖片,同時根據比例放大圖片,此時我們的圖片就可能被放大導致不清晰了。所以我們需要美工MM提供一套你需要支持的最大dpi的圖片,這樣即使用戶的手機分辨率很小,這樣圖片縮小依然很清晰。
- xhdpi首選資源文件夾
上面所說我們提供一套圖片資源就好了,然而我們應該放在哪個文件夾呢?我在某些統計SDK平臺看到,xhdpi是目前最普遍的手機對應的像素密度,所以我們就需要一套放xhdpi的圖片資源就好了,雖然現在手機的發展,xxhdpi和xxxhdpi越來越多了,但是我們暫時就維護好xhdpi一個資源文件夾就好了。
- 設計資源的大小問題
公司的設計MM都會按照一種size來設計、切圖、標記。然而我公司的MM就用iPhone6plus的大小來設計的,有公司會要求設計MM做一套iOS的一套Android的,好羨慕啊~。iPhone6plus是5.5英寸,分辨率是1920*1080,那麼他的dpi計算就是,dpi = √(1920*1920+1080*1080)/5.5 ≈ 401,再根據上面的dpi表,這套圖我們是要放在xxhdpi,大家可能覺得我剛剛說應該首選放在xhdpi,然後這裏就說自己公司的圖就放在xxhdpi,這是因爲不同的公司它的習慣不同,我們根據實際情況進行處理。
適配
可能上面的介紹你已經知道了解了。那麼我接着來說一下如何適配。
我們首先用一個例子來引出:
wrap_content VS dip
wrap_content和dip我們在佈局的時候經常用到,那麼在佈局中他們的效果是怎麼的呢?
- 假設我們在需要放一張180*180px的圖片。看看我們同一個size的測試機,放在不同資源文件夾下的效果。假設我們放在hdpi、xxhdpi下。
我們xml的代碼是
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
- hdpi文件夾效果圖
- xxhdpi文件夾效果圖
上面的效果,我是在420dpi的測試機效果圖,首先我們的圖片是180*180px的size,對於420dpi的測試機,系統首先會對應的xxhdpi尋找資源,(見xxhdpi文件夾效果圖),找到之後顯示180px的尺寸;第二種情況,如果在對應的資源文件夾下找不到,就在最接近的資源文件下找,然後我們只放在hdpi下,系統就只能讀取hdpi下的圖片,那麼屏幕顯示360px(xxhdpi:hdpi = 2:1)。
- 假設我們放一張180*180px的圖片。看看我們同一個資源文件夾xxhdpi,使用不同分辨率的測試機下的效果。假設我們運行在hdpi、xxhdpi的測試機下。
我們xml的代碼是
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
- hdpi測試機
- xxdpi測試機
- 假設我們放一張180*180px的圖片。看看我們同一個資源文件夾xxhdpi,用同一臺測試機,但是我們用不同的xml佈局代碼。
- wrap_content
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
- 固定dip值
<ImageView
android:layout_width="100dip"
android:layout_height="100dip"
android:src="@drawable/ic_launcher" />
上面的效果,我是用1920*1080,420dpi的測試機,在wrap_content情況,180px,就根據像素顯示;在100dip的情況,我們需要換算一下,px = 100x(420/160)= 262.5px。
上面的三種情況,總結起來就是1.圖片應該放哪裏?不同手機怎麼適配資源?3.自適應尺寸跟固定尺寸的效果區別。
支持各種屏幕密度
上面好像都是說圖片放哪裏的問題,接着我們要說的是尺寸適配問題。其實說dip可以去除不同像素密度的問題,使得1dip在不同像素密度上面的顯示的效果大致相同,但是由於Android屏幕設備的多樣式,比如說,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,儘可能少用dip指定某些控件的長寬,再結合權重,大部分情況我們是可以做到適配的了。
但是在我面試的時候,面試總喜歡刁難一下你,轉幾個彎再問你問題的。
我接着介紹四種佈局屏幕適配的方法。
values-XXX*XXX(dp數據)
我們創建我們需要適配的分辨率,在對應的valus下設置我們需要的dimen,單位用dp。我們儘量參考某些統計SDK再配合自己應用最常用的幾款機型做對應的適配。values-sw600dp
這裏的sw代表smallwidth的意思,當你所有屏幕的最小寬度都大於600dp時,屏幕就會自動到帶sw600dp後綴的資源文件裏去尋找相關資源文件,這裏的最小寬度是指屏幕寬高的較小值,每個屏幕都是固定的,不會隨着屏幕橫向縱向改變而改變。values-XXX*XXX(px數據)
下面的方案來自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 / 100f;
}
}
代碼應該很好懂,我們將一個屏幕寬度分爲320份,高度480份,然後按照實際像素對每一個單位進行復制,放在對應values-widthxheight文件夾下面的lax.xml和lay.xml裏面,這樣就可以統一所有你想要的分辨率的單位了,下面是生成的一個320*480分辨率的文件,因爲寬高分割之後總分數和像素數相同,所以x1就是1px,以此類推
<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="x1">1.0px</dimen>
<dimen name="x2">2.0px</dimen>
<dimen name="x3">3.0px</dimen>
<dimen name="x4">4.0px</dimen>
<dimen name="x5">5.0px</dimen>
<dimen name="x6">6.0px</dimen>
<dimen name="x7">7.0px</dimen>
<dimen name="x8">8.0px</dimen>
<dimen name="x9">9.0px</dimen>
<dimen name="x10">10.0px</dimen>
...省略好多行
<dimen name="x300">300.0px</dimen>
<dimen name="x301">301.0px</dimen>
<dimen name="x302">302.0px</dimen>
<dimen name="x303">303.0px</dimen>
<dimen name="x304">304.0px</dimen>
<dimen name="x305">305.0px</dimen>
<dimen name="x306">306.0px</dimen>
<dimen name="x307">307.0px</dimen>
<dimen name="x308">308.0px</dimen>
<dimen name="x309">309.0px</dimen>
<dimen name="x310">310.0px</dimen>
<dimen name="x311">311.0px</dimen>
<dimen name="x312">312.0px</dimen>
<dimen name="x313">313.0px</dimen>
<dimen name="x314">314.0px</dimen>
<dimen name="x315">315.0px</dimen>
<dimen name="x316">316.0px</dimen>
<dimen name="x317">317.0px</dimen>
<dimen name="x318">318.0px</dimen>
<dimen name="x319">319.0px</dimen>
<dimen name="x320">320px</dimen>
</resources>
那麼1080*1960分辨率下是什麼樣子呢?我們可以看下,由於1080和320是3.37倍的關係,所以x1=3.37px。
<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="x1">3.37px</dimen>
<dimen name="x2">6.75px</dimen>
<dimen name="x3">10.12px</dimen>
<dimen name="x4">13.5px</dimen>
<dimen name="x5">16.87px</dimen>
<dimen name="x6">20.25px</dimen>
<dimen name="x7">23.62px</dimen>
<dimen name="x8">27.0px</dimen>
<dimen name="x9">30.37px</dimen>
<dimen name="x10">33.75px</dimen>
...省略好多行
<dimen name="x300">1012.5px</dimen>
<dimen name="x301">1015.87px</dimen>
<dimen name="x302">1019.25px</dimen>
<dimen name="x303">1022.62px</dimen>
<dimen name="x304">1026.0px</dimen>
<dimen name="x305">1029.37px</dimen>
<dimen name="x306">1032.75px</dimen>
<dimen name="x307">1036.12px</dimen>
<dimen name="x308">1039.5px</dimen>
<dimen name="x309">1042.87px</dimen>
<dimen name="x310">1046.25px</dimen>
<dimen name="x311">1049.62px</dimen>
<dimen name="x312">1053.0px</dimen>
<dimen name="x313">1056.37px</dimen>
<dimen name="x314">1059.75px</dimen>
<dimen name="x315">1063.12px</dimen>
<dimen name="x316">1066.5px</dimen>
<dimen name="x317">1069.87px</dimen>
<dimen name="x318">1073.25px</dimen>
<dimen name="x319">1076.62px</dimen>
<dimen name="x320">1080px</dimen>
</resources>
無論在什麼分辨率下,x320都是代表屏幕寬度,y480都是代表屏幕高度。
那麼,我們應該如何使用呢?
首先,我們要把生成的所有values文件夾放到res目錄下,當設計師把UI高清設計圖給你之後,你就可以根據設計圖上的尺寸,以某一個分辨率的機型爲基礎,找到對應像素數的單位,然後設置給控件即可。
但是,還是有個問題,這是因爲由於在生成的values文件夾裏,沒有對應的分辨率,其實一開始是報錯的,因爲默認的values沒有對應dimen,所以我只能在默認values裏面也創建對應文件,但是裏面的數據卻不好處理,因爲不知道分辨率,我只好默認爲x1=1dp保證儘量兼容。這也是這個解決方案的幾個弊端,對於沒有生成對應分辨率文件的手機,會使用默認values文件夾,如果默認文件夾沒有,就會出現問題。
所以說,這個方案雖然是一勞永逸,但是由於實際上還是使用的px作爲長度的度量單位,所以多少和google的要求有所背離,不好說以後會不會出現什麼不可預測的問題。其次,如果要使用這個方案,你必須儘可能多的包含所有的分辨率,因爲這個是使用這個方案的基礎,如果有分辨率缺少,會造成顯示效果很差,甚至出錯的風險,而這又勢必會增加軟件包的大小和維護的難度,所以大家自己斟酌,擇優使用。
4. 百分比佈局
對於網站來說他們就是使用百分比佈局的,所以我們發現他們並沒有出現任何的屏幕適配問題,寫iOS的同學也是使用百分比佈局,我們的Google已經支持百分比的方式佈局了。具體的我們來參考一下【鴻洋】:http://blog.csdn.net/lmj623565791/article/details/46695347;
總結:上述的幾種方法,在不同公司會用不同的方法,但是我們一定對上面的方法瞭解,到時候面試的時候我們可以用最快的語速把你知道的全部表達出來,讓面試官覺得你對屏幕適配這塊很熟悉,由於Android屏幕的多樣式,我們是無法做到絕對的完美的,我們需要的是根據我們的需求,開發週期等方面選擇一個適合我們的一種適配方式。