作Android,必定會接觸到屏幕適配,而屏幕適配的方案也是有多種多樣,這個話題一直沒有中止,最近也是想再研究一下適配的多種方式。 先放一個表格html
密度類型 | 表明的分辨率(px) | 屏幕密度(dpi) | density | 換算(px/dp) | 比例 |
---|---|---|---|---|---|
低密度(ldpi) | 240x320 | 120 | 1dp=0.75px | 0.75 | 3 |
中密度(mdpi) | 320x480 | 160 | 1dp=1px | 1 | 4 |
高密度(hdpi) | 480x800 | 240 | 1dp=1.5px | 1.5 | 6 |
超高密度(xhdpi) | 720x1280 | 320 | 1dp=2px | 2 | 8 |
超超高密度(xxhdpi) | 1080x1920 | 480 | 1dp=3px | 3 | 12 |
density的意思就是1dp等於幾個px像素點 好比density=3,意思是1dp=3pxjava
dp、dip、dpi、ppi 這四個是新手容易混淆的,其中dp和dip是同樣的概念,這個是android特有的一種邏輯單位,和具體設備的物理像素無關。 而dpi和ppi是同樣的概念,這個是一平方英寸裏有多少個像素點的意思。android
無論你在佈局文件中填寫的是什麼單位,最後都會被轉化爲 pxios
好比常使用的:RelativeLayout佈局、LinearLayout佈局、weight、.9.png、svg圖片、ViewStub、include、merge 不長使用的,好比app須要自動適配手機和平板時用到的:佈局別名、smallestWidth限定符 詳情可看這裏:www.jianshu.com/p/ec5a1a306…git
根據以上疑問,咱們來一一解答github
不少時候有經驗的設計人員給咱們的原型裏,已經有了dp值,可是有些設計新人並不知道如何在原型裏標註多大的dp值,並且不少時候設計都是按照ios的分辨率來切圖的,咱們先說按照android標準尺寸切圖的狀況,假設咱們使用1920*1080分辨率的底圖bash
使用1920*1080分辨率做爲底圖設計切圖後,咱們儘可能把切圖放在高dpi文件夾裏(設計底圖分辨率不要過低,如1920*1080就比較清晰),不然放在低dpi文件夾裏的話,若是app安裝在高dpi的手機設備裏,圖片會拉伸,可能會模糊,如今通常至少1920*1080分辨率,這個分辨率的谷歌標準dpi是480,也就是xxhdpiapp
3.1. 若是咱們將切圖放到了res/xxhdpi下面,根據谷歌設計規範這個dpi的density是3,若是1080px*1920px分辨率的底圖中有一個圖片是540px*960px(在density等於3時等同於180dp*320dp),那麼這個圖片使用warp_content的話在1080px*1920px(在density等於3時等同於360dp*640dp)分辨率而且density等於3的設備上顯示時寬度正好是屏幕寬度的一半(比較dp的話是540px/3density=180dp是360dp的一半,比較px的話是540px是1080px的一半)框架
3.2. 若是咱們將切圖位置不變,仍然放到了res/xxhdpi(density=3)下面,那麼該540px*960px分辨率的圖片在xhdpi(density=2)的設備裏使用warp_content自適應時的分辨率和dp是多少呢?iphone
3.3. 若是咱們將剛纔540px*960px的切圖放到res/xhdpi下面的話(xhdpi的density是2,因此等同於270dp*480dp),那麼這個圖片使用warp_content的話在xhdpi(density=2時xhdpi是360dp寬640dp高)的設備上顯示時這個圖片寬度要大於屏幕寬度的一半,不信的能夠試一下,我試過了沒錯(比較dp的話是270dp是360dp的0.75倍,比較px的話是540px是720px的0.75倍)
備註:以上計算方式必須知道設備的dpi或者density其中一個,不然沒法計算。(density=dpi/160)
假如知道某個圖片在某個dpi文件夾裏的warp_content時的px值,想知道這個圖片放在其餘dpi文件夾裏的warp_content時的px值,能夠經過 px/當前dpi設備的density 獲得這個圖片在當前dpi下面的dp值,而後根據該 dp值*其餘dpi設備的density 獲得該圖片放在其餘dpi設備裏的px值。
因此一個新項目咱們可讓設計按照某個谷歌標準分辨率作底圖,而後根據上面的規則咱們就知道圖中對應的px在某個dpi文件夾裏是多少dp。
簡單說,就是窮舉市面上全部的Android手機的寬高像素值,而後建立一批不一樣分辨率下的dimen文件,其中值的單位是px:
設定一個基準的分辨率,其餘分辨率都根據這個基準分辨率來計算,在不一樣的尺寸文件夾內部,根據該尺寸編寫對應的dimens文件。
好比以480x320爲基準分辨率
寬度爲320,將任何分辨率的寬度整分爲320份,取值爲x1-x320
高度爲480,將任何分辨率的高度整分爲480份,取值爲y1-y480
那麼對於480*800的分辨率的dimens文件來講,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
...
這個時候,咱們用UI設計界面做爲基準分辨率,好比UI設計界面是640px*960px,而後咱們建立values-640x960,而後建立一堆dimen值,分別是x1-x640,值從1px-640px,若是咱們要使用1000px怎麼辦呢?咱們能夠將dimen的範圍寫大一些也能夠的,只要比例同樣就行。
而後咱們能夠根據這個基準分辨率建立其餘分辨率的文件,好比建立 values-480x800,x1就是480/640=0.75px,其餘值根據此比例來生成。
當APP運行在不一樣分辨率的手機中時,這些系統會根據這些dimens引用去該分辨率的文件夾下面尋找對應的值。這樣基本解決了咱們的適配問題,並且極大的提高了咱們UI開發的效率。
可是這個方案有一個致命的缺陷,那就是須要精準命中才能適配,好比1920*1080的手機就必定要找到1920*1080的限定符,不然就只能用統一的默認的dimens文件了。而使用默認的尺寸的話,UI就極可能變形,簡單說,就是容錯機制不好。
不過這個方案有一些團隊用過,咱們能夠認爲它是一個比較成熟有效的方案了。
這個和上面的區別是窮舉市面上全部手機的dp值,dimen的單位是dp,該方法解決了上面方法1的缺點,即便某個dp沒有覆蓋,系統也會尋找小於或等於該dp的文件,而後用該文件適配。這種機制和上文提到的寬高限定符適配原理上是同樣的,都是系統經過特定的規則來選擇對應的文件。
這種適配方式的dimen文件的生成的規則和上面同樣,也是先設置一個基準dp,由於系統會根據當前設備的最小dp去選擇文件夾,因此咱們把設計圖的px當成dp做爲基準dp就能夠了,舉個例子,咱們的UI設計圖是640px*960px,咱們把它當成640dp*960dp,而後咱們建立基準dp文件夾values-sw640dp,咱們能夠從1建立到分辨率的最大值,好比x1=1dp,x640=640dp,x960=960dp,而後咱們能夠其餘swdp文件夾,好比建立values-sw480dp文件夾,480/640=0.75,因此x1=0.75dp,以此類推x640*0.75=480dp,x960*0.75=720dp,看到了吧,咱們直接用UI設計圖的分辨率做爲基準分辨率便可,而後使用的時候設計圖中的10px咱們用x10就能夠了,很方便。一樣的咱們也能夠建立layout-swdp,在不一樣dpi的設備裏系統會自動選擇對應的layout
思考一下:若是一個相同name的dimen在values-分辨率文件夾和values-swdp文件夾裏都定義了的時候系統會使用哪一個值?我測試了一下,系統會優先使用values-swdp文件夾裏的值。
這個方案的缺點:
在佈局中引用 dimens 的方式,雖然學習成本低,可是在平常維護修改時較麻煩
侵入性高,若是項目想切換爲其餘屏幕適配方案,由於每一個 Layout 文件中都存在有大量 dimens 的引用,這時修改起來工做量很是巨大,切換成本很是高昂
沒法覆蓋所有機型,想覆蓋更多機型的作法就是生成更多的資源文件,但這樣會增長 App 體積,在沒有覆蓋的機型上還會出現必定的偏差,因此有時須要在適配效果和佔用空間上作一些抉擇
若是想使用 sp,也須要生成一系列的 dimens,致使再次增長 App 的體積
不能自動支持橫豎屏切換時的適配,如上文所說,若是想自動支持橫豎屏切換時的適配,須要使用 values-wdp 或 屏幕方向限定符 再生成一套資源文件,這樣又會再次增長 App 的體積
咱們強制將density修改成谷歌標準值,也就至關於咱們強制把設計人員給的圖片轉爲谷歌某個標準分辨率,這樣咱們上面的計算方法就有效了,經過測試某個切圖發現不一樣dp寬度的模擬器中該圖片在屏幕的比例都是一致的。可是修改了系統的density值以後,整個佈局的實際尺寸都會發生改變,若是想要在老項目文件中使用,恐怕整個佈局文件中的尺寸均可能要從新按照設計稿修改一遍才行。所以,若是你是在維護或者改造老項目,使用這套方案就要三思了。
我先發一下美團方式的計算公式
public class Test {
private static float sNonCompatDensity;
private static float sNonCompatScaledDensity;
public static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application, final int designWidthDp) {
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sNonCompatDensity == 0) {
sNonCompatDensity = appDisplayMetrics.density;
sNonCompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNonCompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
final float targetDensity = appDisplayMetrics.widthPixels / ((float) (designWidthDp));
final float targetScaledDensity = targetDensity * (sNonCompatScaledDensity / sNonCompatDensity);
final int targetDensityDpi = (int) (160 * targetDensity);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
}
複製代碼
美團這種方式如何使用呢,好比設計人員的底圖分辨率是W*H(豎屏,而且W<H),若是你要適配1080*1920(xxhdpi)也就是說你想把切圖放到xxhdpi的文件夾下面,而1080*1920的谷歌標準是360dp寬(1080/xxhdpi的density),因此Wpx相對於1080px底圖也就是$$\cfrac{Wpx}{1080px}=\cfrac{Wdp}{360dp}$$而後計算獲得Wdp的值,而後把這個圖片和切圖放到xxhdpi下面,在Activity的onCreate方法的setContentView前面調用Test.setCustomDensity(this, application,Wdp)
,而後顯示效果是和設計圖同樣的
舉個例子,好比設計人員的底圖是500*1000,其中有個切圖是500*1000,而後若是你要適配1080*1920(xxhdpi),而1080*1920的谷歌標準是360dp寬(1080/xxhdpi的density),因此500px相對於1080px底圖也就是500/1080*360=167dp,而後把這個圖片放到xxhdpi下面,而後調用Test.setCustomDensity(this, application,167)
,這樣就會在任何模擬器裏這個圖片看起來寬度都是正好佔滿屏幕寬度。
而若是要適配720*1280(xhpdi),而720*1280的谷歌標準是360dp寬(720/xhdpi的density),因此500px相對於1080px底圖也就是500/1080*360=167dp,而後把這個圖片放到xhpdi下面,而後調用Test.setCustomDensity(this, application,167)
而若是要適配480*800(hpdi),而480*800的谷歌標準是320dp寬(480/hdpi的density),因此500px相對於480px底圖也就是500/480*320=333dp,而後把這個圖片放到hpdi下面,而後調用Test.setCustomDensity(this, application,333)
可是這個方式也有缺點: 這個方案依賴於設計圖尺寸,可是項目中的系統控件、三方庫控件、等非咱們項目自身設計的控件,它們的設計圖尺寸並不會和咱們項目自身的設計圖尺寸同樣 當這個適配方案不分類型,將全部控件都強行使用咱們項目自身的設計圖尺寸進行適配時,這時就會出現問題,當某個系統控件或三方庫控件的設計圖尺寸和和咱們項目自身的設計圖尺寸差距很是大時,這個問題就越嚴重。 解決方案有兩種:
方案 1 調整設計圖尺寸,由於三方庫多是遠程依賴的,沒法修改源碼,也就沒法讓三方庫來適應咱們項目的設計圖尺寸,因此只有咱們自身做出修改,去適應三方庫的設計圖尺寸,咱們將項目自身的設計圖尺寸修改成這個三方庫的設計圖尺寸,就能完成項目自身和三方庫的適配
這時項目的設計圖尺寸修改了,因此項目佈局文件中的 dp 值,也應該按照修改的設計圖尺寸,按比例增減,保持與以前設計圖中的比例不變
可是若是爲了適配一個三方庫修改整個項目的設計圖尺寸,是很是不值得的,因此這個方案支持以 Activity 爲單位修改設計圖尺寸,至關於每一個 Activity 均可以自定義設計圖尺寸,由於有些 Activity 不會使用三方庫 View,也就不須要自定義尺寸,因此每一個 Activity 都有控制權的話,這也是最靈活的
但這也有個問題,當一個 Activity 使用了多個設計圖尺寸不同的三方庫 View,就會一樣出現上面的問題,這也就只有把設計圖改成與幾個三方庫比較折中的尺寸,才能勉強緩解這個問題
方案 2 第二個方案是最簡單的,也是按 Activity 爲單位,取消當前 Activity 的適配效果,改用其餘的適配方案
該方案的補充與擴展: juejin.im/post/5b7faf…
至此完成本篇文章,可能有的地方有些囉嗦,我是想盡量講的詳細一些,我把jess中的文章有些不清楚的地方我添加了一些解釋
其餘一些適配方式:
使用鴻洋大神的軟件生成大量px文件放到項目中 這種方式,是根據dp、density、px換算出來一堆px文件,分辨對應不一樣分辨率的手機,能解決大部分的適配問題,可是若是遇到分辨率比較高可是屏幕尺寸比較大的時候,這個設備的dpi會比較低,而後就會有些問題,並且這樣也會有一大堆"values-寬X高"文件夾,裏面有一大堆的px文件,增長apk體積。這時候應該能夠再建立相似values-160dpi這種文件夾來解決,並且能夠建立values-160dpi-1024x600這種文件夾,這些文件夾能夠混用,優先用更精確的那個。
使用鴻洋大神的AutoLayout框架 這個方式也不錯,裏面的源碼我還沒仔細看,咱們公司也用的這種方式,不過有些控件會有問題,針對這些控件鴻洋有一些重寫,不過沒有的就須要本身去寫了
約束佈局(ConstraintLayout),這個我還沒怎麼去了解,待這幾天看看研究一下再來完善該篇文章,在這篇文檔裏:developer.android.com/reference/a… ,谷歌明確表示廢棄了百分比佈局庫,而應該使用約束佈局。
使用pt物理單位 源連接: www.apkbus.com/blog-177177… 涉及到的代碼: github.com/Firedamp/Ru… 在上面這個連接裏有段代碼是
resources.getDisplayMetrics().xdpi = size.x/designWidth*72f;
複製代碼
在android系統裏有個方法是這樣的
public static float applyDimension(int unit, float value,
DisplayMetrics metrics){
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
複製代碼
value * metrics.xdpi * (1.0f/72) = 該控件實際顯示的px值
能夠換算爲metrics.xdpi = 該控件實際顯示的px值/value*72
,其中value
是該控件在設計圖顯示的pt值大小 而咱們要保證這個控件在任何分辨率下都相對於屏幕大小有固定的比例,只須要讓 控件實際顯示的px值/value=實際屏幕顯示的px值/實際屏幕的pt值大小
,也就是resources.getDisplayMetrics().xdpi = size.x/designWidth*72f
參考文章較多,記錄幾個 www.jianshu.com/p/c772cf494… www.apkbus.com/blog-177177…