前言
Android螢屏解析度千奇百怪,怎么讓app在不同的解析度的設備上“看起來一樣”呢?
你也許還有以下疑惑:
- px、dp、sp區別與作用
- mipmap和drawable區別與作用
- mdpi hdpi xhdpi的圖片資源有什么區別
- 如何適配不同密度下的圖片資源
- 不同解析度的設備如何適配寬度
- dpi是怎么確定的
這篇文章將會針對以上問題一一解答,
基本單位
px
Pixels 我們看到螢屏上的影像由一個個像素組成,像素里包含色彩資訊,
如常說的手機解析度:1080 x 1920 指的是手機寬度可展示1080像素,高度可展示1920像素,
ppi
Pixels Per Inch 每英寸長度所具有的像素個數,單位面積內像素越多,影像顯示越清晰,
ppi一般用在顯示幕、手機、平板等描述螢屏精細度,
dpi
Dots Per Inch 每英寸長度所具有的點數,
dpi一般用來描述列印(書本、雜志、電報)的精細度
dp/dip
density-independent pixels (device-independent pixels 我查了一下,官網更多時候使用前者,有的時候也顯示后者),dip是縮寫,也可以更簡單些稱作dp,該單位的目的是屏蔽不同設備密度差異,后面細說,
sp
Scalable pixels 用于設定字體,在用戶更改字體大小時候會適配,
簡單例子
澄清了基本概念,我們現在從一個例子開始說明以上單位之間的區別與聯系,
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/big"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:layout_gravity="center"
android:background="@color/green"
android:layout_width="200px"
android:layout_height="200px">
</View>
</FrameLayout>
布局檔案里有個View,長寬都是200px,分別在解析度為480(寬)x800(高)簡稱A設備、1080(寬)x1920(高)簡稱B設備,效果如下:

左邊是A設備,右邊是B設備,問題出來了,同樣長寬都是200px,為啥A設備顯示很大,B設備顯示很小呢?你可能會說B設備的橫向解析度1080比A設備的480大,所以在B設備上看起來比較小,來看看A、B設備橫向到底是多少英寸,怎么來計算呢?這時候就需要用到ppi了,既然知道橫向的像素點個數,也知道每英寸能容納的像素點,當然可以得知橫向的尺寸了,
DisplayMetrics.java
/**
* The exact physical pixels per inch of the screen in the X dimension.
*/
public float xdpi;
/**
* The exact physical pixels per inch of the screen in the Y dimension.
*/
public float ydpi;
其中一種方式獲取DisplayMetrics物件:
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
A設備寬度尺寸:480(px)/240(ppi)=2inch
B設備寬度尺寸:1080(px)/420(ppi)=2.5inch
可以看出,A、B設備尺寸差別不大,A設備ppi=240 B設備ppi=420,明顯地看出B設備單位長度上比A設備能夠容納更多的像素,因此同樣的200px,B設備只需要較小的尺寸就能夠顯示,因此在B設備上的view看起來比A設備小很多,
知道了問題的原因,然而顯示的效果卻不能接受,
我們想要的效果是:同一大小的view在不同的設備上“看起來一樣大”
我們總不能自己判斷每個設備的ppi,然后計算實際需要多少像素,再動態設定view的大小吧,那layout里的靜態布局大小就無法動態更改適應了,想當然的能有一個統一的地方替我們轉換,沒錯!Android系統已經幫我們實作了轉換,接下來就是dpi、dp出場了,
引入dpi、dp
Android系統使用dpi來描述螢屏的密度,使用dp來描述密度與像素的關系,
A設備dpi=240
B設備dpi=420
Android系統最終識別的單位是px,怎么將dpi和px關聯起來呢?,答案是dp,
Android規定當dpi=160時,1dp=1px,當dpi=240時,1dp=1.5px,依此類推,并且給各個范圍的dpi取了簡易的名字加以直觀的識別,如120<dpi<=160,稱作為mdpi,120<dpi<=240 稱作hdpi,最終形成如下規則:
ldpi(value <= 120 dpi)
mdpi(120 dpi < value <= 160 dpi)
hdpi(160 dpi < value <= 240 dpi)
xhdpi(240 dpi < value <= 320 dpi)
xxhdpi(320 dpi < value <= 480 dpi)
xxxhdpi(480 dpi < value <= 640 dpi)
現在知道了dp能夠在不同dpi設備上對應不同px,相當于中間轉換層,我們只需要將view長寬單位設定為合適的dp,就無需關注設備之間密度差異,系統會幫我們完成dp-px轉換,將我們之前的例子稍微更改,再看看效果驗證一下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/big"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:layout_gravity="center"
android:background="@color/green"
android:layout_width="200dp"
android:layout_height="200dp">
</View>
</FrameLayout>

這里看起來還是不一樣呢?[注1]
綜上所述,dp作為中間單位為我們屏蔽了不同密度設備差異,這也是為啥dp/dip叫做“設備(密度)無關像素”的原因,
mipmap圖片資源檔案
通過上面對dp的了解,我們知道在設定view大小、間距時使用dp能最大限度地屏蔽設備密度之間的差異,可能你就會問了,那bitmap展示的時候如何適配不同密度的設備呢?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight());
}
private void init() {
String path = Environment.getExternalStorageDirectory() + "/Download/photo1.jpg";
bitmap = BitmapFactory.decodeFile(path);
paint = new Paint();
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
RectF rectF = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.drawBitmap(bitmap, src, rectF, paint);
}
自定義view從磁盤上加載一張圖片,并將之顯示在view上,view的大小決定于bitmap大小,依舊以上述A、B設備為例,展示結果如下:

左邊是A設備,右邊是B設備,
明顯地看出,在A設備顯示比B設備大很多,實際上和我們之前用px來描述view的大小原理是一樣的,bitmap的寬、高都是px在描述,而bitmap決定了view的寬、高,最終導致A設備和B設備上的view大小(寬、高像素)是一樣的,而它們螢屏密度又不相同,因此產生了差異,
那不會每次都需要我們自己根據螢屏密度來轉換bitmap大小吧?幸運的是,Android已經為我們考慮到了,

如上圖,在Android Studio創建工程的時候,默認在res下創建mipmap目錄,這些mipmap目錄按照密度分為mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi,看起來都在“一個“mipmap”目錄下,實際上分為不同的目錄:

生成不同密度的目錄有什么作用?
A設備dpi=240,根據dpi范圍,屬于hdpi
B設備dpi=420,根據dpi范圍,屬于xxhdpi
圖片原始尺寸:photo1.jpg(寬高 172px-172px)
當我們想要在不同密度設備上顯示同一張圖片并且想要“看起來一樣大時”,假設設計的時候以hdpi為準,放置photo1.jpg為172*172,那么根據計算規則在xxhdpi上需要設定photo1.jpg為:
scale = 480 / 240 = 2
width = 172 * 2 = 344
height = 172 * 2= 344
注:這里為什么要放大?可以這么理解,因為B設備密度大,通常來說密度越大單位尺寸內需要的像素越多,假設A設備上172*172占據1inch面積,那么為了能夠在B設備上填充滿相同的面積需要更多的像素,因此B設備上的圖片解析度應該更大(這里說的通常是因為真正決定設備單位尺寸內容納的像素個數的因素是ppi,有些設備dpi比較大,但是ppi反而小)
現在hdpi和xxhdpi目錄下分別存放了同名圖片:photo1.jpg,只是大小不同,當程式運行的時候:
A設備發現自己密度屬于hdpi,它會直接到hdpi下尋找對應的photo1.jpg并顯示
B設備發現自己密度屬于xxhdpi,它會直接到xxhdpi下尋找對應的photo1.jpg并顯示
來看看效果:

左邊A設備,右邊B設備
針對不同的密度設計不同的圖片大小,最大限度保證了同一圖片在不同密度設備上表現“看起來差不多大”,
來看看A、B設備上圖片占記憶體大小:
A設備 172 * 172 * 4 = 118336 ≈ 116k
B設備 344 * 344 * 4 = 473344 ≈ 462k
注:決議bitmap時,默認inPreferredConfig=ARGB_8888,也就是每個像素有4個位元組來存盤
說明在B設備上顯示photo1.jpg需要更多的記憶體,
上邊只是列舉了hdpi、xxhdipi,同理對于mdpi、xhdpi、xxxhdpi根據規則放入相應大小的圖片,程式會根據不同的設備密度從對應的mipmap檔案夾下加載資源,如此一來,我們無需關注bitmap在不同密度設備上顯示問題了,
圖片資源檔案的加載
在mipmap各個檔案夾下都放置同一套資源的不同尺寸檔案似乎有點太占apk大小,能否只放某個密度下圖片,其余的靠系統自己適配呢?
現在只保留hdpi下的photo1.jpg圖片,看看在A、B設備上運行情況如何:

看起來和上張圖差不多,說明系統會幫我們適配B設備上的圖片,
再來看看A、B設備上圖片占記憶體大小:
先看A設備:

再看B設備:

A設備 172 * 172 * 4 = 118336 ≈ 116k
B設備 301 * 301 * 4 = 362404 ≈ 354k
對比photo1.jpg 分別放在hdpi、xxhdpi和只放在hdpi下可以看出:B設備上圖片所占記憶體變小了,為什么呢?接下來從原始碼里尋找答案,
構造Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.photo1);
A、B設備同樣加載hdpi/photo1.jpg,回傳的bitmap大小不相同,我們從這方法開始一探究竟,
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
//根據資源id,構造Value物件,這里面需要關注的變數:density
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
If it happened on close, bm is still valid.
*/
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable BitmapFactory.Options opts) {
validate(opts);
if (opts == null) {
opts = new BitmapFactory.Options();
}
if (opts.inDensity == 0 && value != null) {
//通過value里的density給options里的inDensity賦值
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//獲取設備螢屏密度并賦予opts.inTargetDensity
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
//確定option inDensity、inTargetDensity 后傳入jni層加載bitmap
return decodeStream(is, pad, opts);
}
上面涉及到的關鍵點是density,分別是TypedValue的density和Options的density,
先來看看TypedValue density:
/**
* If the Value came from a resource, this holds the corresponding pixel density.
* */
public int density;
簡單解釋:表示該資源從哪個密度檔案夾下取的;比如A、B設備取hdpi下的photo1.jpg,那么此時density=240
再來看看Options density
* The pixel density to use for the bitmap. This will always result
* in the returned bitmap having a density set for it
public int inDensity;
* The pixel density of the destination this bitmap will be drawn to.
* This is used in conjunction with {@link #inDensity} and
* {@link #inScaled} to determine if and how to scale the bitmap before
* returning it.
public int inTargetDensity;
簡單解釋:inDensity表示該資源來源于哪個密度的檔案夾,該值從TypedValue獲取;inTargetDensity表示該資源將要顯示在哪個密度的設備上,在構造Bitmap時,會根據inDensity與inTargetDensity決定Bitmap放大縮寫的倍數,
計算公式如下:
needSize = (int)(size * ((float)inTargetDensity / inDensity) + 0.5) (四舍五入)
現在分析B設備加載hdpi/photo1.jpg如何做的:
1、hdpi密度是240 因此Options.inDesnity = 240
2、B設備密度是420 因此Options.inTargetDensity = 420;
3、B設備回傳bitmap大小=172 * 420 / 240 = 301px
和我們之前除錯的結果一致,
Density匹配規則
B設備是怎么決定使用hdpi下的圖片資源呢?
根據實驗(嘗試找了原始碼,沒怎么看懂,因此只是做了實驗,可能在不同密度設備上找尋規則不一樣):B設備先找屬于自己密度范圍檔案夾下的圖片,B設備屬于xxhdpi,先查看xxhdpi有沒有photo1.jpg,如果沒有則往更高的密度找,比它高的密度是xxxhdpi,還是沒有,則往低密度找,找xhdpi,沒有再找hdpi,找到了則回傳構造好的TypedValue,剩下的就是我們前面分析的,
既然我們只想放某個密度下的一份切圖,該放哪個密度下呢?從系統尋找規則看,更推薦放置在更高密度下的,因為如果放在低密度下,那么當運行在高密度設備上時,圖片會進行放大,可能導致不清晰,我一般習慣放在xxhdpi下,
drawable和mipmap不同密度檔案夾
Android Studio默認創建了不同密度的mipmap檔案夾,默認放置了ic_launcher.png,我們普通的切圖該放drawable還是mipmap下呢?對于這個問題網上也是眾說紛紜,實際上對于我們來說,關注的重點是圖片放在drawable或者mipmap,加載出來bitmap是否有差異,如果沒有差異放在哪就看習慣了,通過實踐,普通的切圖放drawable和mipmap下加載出來的bitmap是沒有差異的,只不過用drawable的話需要自己創建不同密度的檔案夾,我習慣于放在drawable下(啟動圖示logo還是放在mipmap下),
螢屏寬度適配
前邊**[注1]**留了個問題,我們使用dp來表示view的大小了,為啥兩個看起來還是有些差距?下面我們更加直觀地看一個例子,
A設備dpi=240 密度1.5 解析度(寬高px):480 * 800
B設備dpi=420 密度2.625 解析度(寬高px):1080 * 1794
換算成dp
A設備解析度:320dp * 533dp
B設備解析度:411dp * 683dp
依舊是上邊的例子:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/big"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="@+id/iv"
android:background="@color/green"
android:layout_gravity="center"
android:layout_width="320dp"
android:layout_height="320dp"/>
</FrameLayout>
將view寬高分別設定為320dp,看看效果:

左邊A設備,右邊B設備
可以看出同樣的320dp大小,A設備鋪滿了螢屏,而B設備沒有,這效果顯然是不能接受的,Android考慮到不同設備寬高不同,推出了"寬高限定符",以A、B設備為例:
在res檔案夾下創建檔案夾:
values-800x480
values-1794x1080
假設設計師出圖是按照800x480,那么我們創建dimen檔案的時候
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="px1">1px</dimen>
<dimen name="px2">1px</dimen>
...
<dimen name="px100">100px</dimen>
<dimen name="px101">101px</dimen>
</resources>
該檔案放在values-800x480檔案夾下,
根據解析度比例算出1794x1080的dimen值
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="px1">2.24px</dimen>
<dimen name="px2">4.48px</dimen>
...
<dimen name="px100">224px</dimen>
<dimen name="px101">226.24px</dimen>
</resources>
這樣子,A、B設備加載資源的時候使用對應解析度限定符下的px,如果找不到再找默認值,可以在一定程度上解決螢屏寬高碎片化適配問題,
但是這樣子的限定比較嚴格,需要測驗各種解析度,后來Android又推出了"smallest-width"簡稱最小寬度限制,
A設備寬320dp
B設備寬411dp
假設設計師切圖示準螢屏寬是320dp(A設備),那么可以定義如下dimen.xml檔案
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp1">1dp</dimen>
<dimen name="dp2">2dp</dimen>
<dimen name="dp320">320dp</dimen>
</resources>
該檔案放在values-sw320dp檔案夾下
根據規則,計算B設備dimen.xml
scale = targetWidth/baseWidth=411/320≈1.28
value = scale * baseValue
得出:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp1">1dp</dimen>
<dimen name="dp2">3dp</dimen>
<dimen name="dp320">410dp</dimen>
</resources>
現在我們繼續來看之前的view
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/big"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="@+id/iv"
android:background="@color/green"
android:layout_gravity="center"
android:layout_width="@dimen/dp320"
android:layout_height="@dimen/dp320"/>
</FrameLayout>
通過對dimen參考,A設備尋找和自己寬度一樣的dimen檔案,找到values-sw320dp,dp320=320dp,B設備尋找和自己寬度一樣的dimen檔案,找到values-sw411dp,dp320=410dp,這樣子同樣的dp320,得出不同的值,就適配了螢屏寬度不同的問題,
看看效果:

這次B設備也鋪滿了屏寬,
1、如果B設備找不到values-sw411dp,那么會繼續往下尋找(比自己寬度小的),比如找到values-sw390dp,就會使用里面的值
2、為什么高度沒有限定呢?因為對于豎直方向上來說,我們是可以設計為滾動模式的,因此對于高度的適配沒那么敏感
綜上,為了適配不同螢屏大小,推薦使用dp+smallest-width,
如何獲取dpi
DisplayMetrics.java
private static int getDeviceDensity() {
// qemu.sf.lcd_density can be used to override ro.sf.lcd_density
// when running in the emulator, allowing for dynamic configurations.
// The reason for this is that ro.sf.lcd_density is write-once and is
// set by the init process when it parses build.prop before anything else.
return SystemProperties.getInt("qemu.sf.lcd_density",
SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}
獲取設備dpi最終都是從這方法獲取的,實際上就是讀取系統的組態檔,因此我們也可以通過adb shell 獲取:
HWTAS:/ $ wm size
Physical size: 1080x2340
HWTAS:/ $
HWTAS:/ $ getprop ro.sf.lcd_density
480
HWTAS:/ $ wm density
Physical density: 480
HWTAS:/ $
可以看出dpi是系統配置好的,當然有些手機是可以設定解析度的,設定之后我們查看解析度:
HWTAS:/ $ wm density
Physical density: 480
Override density: 320
HWTAS:/ $
HWTAS:/ $
HWTAS:/ $ wm size
Physical size: 1080x2340
Override size: 720x1560
HWTAS:/ $
解析度變低了,dpi也變小了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/305729.html
標籤:其他
