我们一直知道 Android 可以使用 dp、sp 完成简单的适配,那你真的理解了么?先来看几个问题:
- dp 是如何进行适配的?
- dp 和 px 是如何换算的?
- sp 和 dp 的区别?
- dp 适配为什么会有偏差?
- 如何解决 dp 适配的偏差,达到完美适配?
下面我们就来看下源码,解决这些问题。
概述
在 android.util 包下,有个重要的类就是 DisplayMetrics
,它主要是记录显示县官的一些信息,比如大小,密度,缩放系数等等。
如果要想获取到其中的信息很简单,可以通过上下文去拿,例如:
DisplayMetrics dm = context.getResources().getDisplayMetrics();
或者按照源码注释上的示例:
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
这个类有一大坨常量:
public static final int DENSITY_LOW = 120;
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_TV = 213;
public static final int DENSITY_HIGH = 240;
public static final int DENSITY_260 = 260;
public static final int DENSITY_280 = 280;
public static final int DENSITY_300 = 300;
public static final int DENSITY_XHIGH = 320;
public static final int DENSITY_340 = 340;
public static final int DENSITY_360 = 360;
public static final int DENSITY_400 = 400;
public static final int DENSITY_420 = 420;
public static final int DENSITY_440 = 440;
public static final int DENSITY_XXHIGH = 480;
public static final int DENSITY_560 = 560;
public static final int DENSITY_XXXHIGH = 640;
其实最基本的只有DENSITY_LOW
、DENSITY_MEDIUM
、DENSITY_HIGH
、DENSITY_XHIGH
、DENSITY_XXHIGH
、DENSITY_XXXHIGH
,这些都是标准的、主要DPI,其它的都是为了适配引申出来的次要的、介于两者之间的DPI。
DPI 就是 dots-per-inch,每一英寸的像素点。
还有个概念是 PPI,全称是 pixel-per-inch,DPI 和 PPI 是两个不同的概念,即便它们的值有时是一样的,区别嘛...很难描述,大家自己找下资料,这里提到的目的是,有些博客把这两个概念混为一谈是不正确的。
这里主要为大家解读三个参数:
-
densityDpi
:设备的DPI,也就是上面那些常量值,是由手机厂商设置的。 -
density
:缩放系数,这个下面会详细讲解。 -
scaledDensity
:字体缩放系数,和density
一样,不过可以受用户设置的字体大小的偏好进行调整。
基础知识
我们先来看下这几个值的赋值,我们发现一个方法,看名字应该是设置默认值。
public void setToDefaults() {
...
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
...
}
1. densityDpi
先从最简单的下手:
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
public static int DENSITY_DEVICE = getDeviceDensity();
private static int getDeviceDensity() {
return SystemProperties.getInt("qemu.sf.lcd_density",
SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}
上面代码很好理解,densityDpi 就是从系统属性取值,如果qemu.sf.lcd_density
的值取不到,就取 ro.sf.lcd_density
的,还取不到,就使用默认值,也就是 160 了。
qemu.sf.lcd_density
和 ro.sf.lcd_density
,手机厂商会写到系统属性中,这样一来,开发者就可以很方便的获取到设备真是的 DPI 了。
2. DENSITY_DEFAULT
这个还是有必要提一句,不要被名字蒙蔽了,它不仅仅是默认值这么简单!
/**
* The reference density used throughout the system.
*/
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
注释很重要!!!
注释很重要!!!
注释很重要!!!
整个系统使用的参考密度,这个可以理解为基准,后面计算 density 的缩放系数,都是以它为基准的。
3. density
用 DPI 除以基准 DPI,得到的就是density(缩放系数)。
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
那么应该如何理解呢?
如果一个 160dpi 的设备,那么 density 就是 1。
如果一个 120dpi 的设备,那么 density 就是 0.75。
如果一个 240dpi 的设备,那么 density 就是 1.5。
还不理解?
在 1 英寸内有 160 像素点的设备上,缩放系数就是 1。
在 1 英寸内有 120 像素点的设备上,缩放系数就是 0.75。
在 1 英寸内有 240 像素点的设备上,缩放系数就是 1.5。
4. scaledDensity
与 density 一样的,也是缩放系数,默认情况和 density 的值是相等的。
scaledDensity = density;
与 density 的区别是,scaledDensity 会受用户设置显示字体的大小进行缩放。
上面几个关键概念理解清楚了,下面就要说下,如何使用dp和sp完成屏幕的适配了。
dp 和 sp 是如何适配的
1. dp
dp 是 Android 中的一个适配的单位,用来表示大小。
如果设置控件的高度为 20dp,那么会发生什么?如何在不同设备上进行适配的呢?下面会解答这些问题。
上面讲到的 density(缩放系数) 就派上用场了,通过 dp 值乘以 density 就得到了最终的像素值。
这也就是网上经常流传的换算比例:
在 240 * 320 分辨率,DPI为 120,density 是 0.75,1dp=0.75px。
在 320 * 480 分辨率,DPI为 160,density 是 1,1dp=1px。
在 480 * 800 分辨率,DPI为 240,density 是 1.5,1dp=1.5px。
...
以此类推看下图:
那么这个DPI是如何计算的?
分辨率为 480*800 的设备举例,在 3.8 寸的屏幕下,DPI 应该约等于 240。
计算公式:
通过公式很容易发现一个问题,DPI 受到分辨率和屏幕尺寸两个值影响的,因为它的定义是,每一英寸的像素点,所以相同分辨率下,不同的屏幕尺寸的设备的 DPI 也是有可能不同的。
这也就说明,Android中我们经常提到的 ldpi,mdpi,hdpi 等等这些规格只能与 DPI 挂钩,并不能由设备分辨率进行区分 ldpi,mdpi,hdpi 这些规格,只能说由于手机尺寸比较接近,大多数的情况下,都符合在上面表格。
知道上面的概念了,这就能理解,在不同DPI设备上 20dp 如何进行适配了。
在 120dpi 的设备上,会乘以 0.75 的缩放系数转换为 15px 展示到界面上。
在 160dpi 的设备上,会乘以 1 的缩放系数转换为 20px 展示到界面上。
在 240dpi 的设备上,会乘以 1.5 的缩放系数转换为 30px 展示到界面上。
...
以此类推,实现了适配。
2. sp
sp 是 Android 中的一个适配的单位,用来表示字体大小。
可以理解为和 dp 是一样的,区别是,它是以 scaledDensity 作为缩放系数,默认情况和 density 的值是一样的,但是在调整系统字体大小时,就会随着字体大小的改变而缩放了。
3. TypedValue
上面我说了,dp、sp 转换 px 会乘以缩放比例,计算出真实的 px 值,口说无凭,来看下 TypedValue
的源码:
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;// dp乘以density缩放系数
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;// sp乘以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;
}
如何使用 dp 达到完美适配
理解了这些知识,有什么用?除了面试时可以装一波,更重要的事,可以完善 Android 的 dp 适配机制。
经常使用 dp 适配会发现,只是能达到大致的适配效果,并不完美,会有多个方面造成不准确:
- 设备厂商设置的 DPI 是否准确。
- Android 提供的 DPI 有限,不一定会覆盖到所有设备厂商。
- DPI 的计算涉及到分辨率和屏幕尺寸,计算出来的值,都会与系统提供的 DPI 有所偏差。
问题说完了,那么如何利用这套 dp 适配的机制,达到完美适配呢?
density、densityDpi、scaledDensity 这些属性都是可以进行修改的,我们要利用这个搞点事情。
dp 适配偏差都是由于 DPI 不准确,导致 density 缩放系数不准确,出现的问题,如果我们拿到精准的缩放比例,是不是就能将 dp 完美转换为 px 了啊?
我们可以换一下思路,我们要求美工给出一套以 dp 为单位的基准图,例如:基准宽为 320dp,运行在 1080*1920 的设备上。
1. 运行时获取到设备精准的 density
我们可以很轻易的得到缩放系数是 1080 / 320,也就是3.375。
float newDensity = dm.widthPixels / 320;
不要问我,如何在运行时获取到宽高这样的参数,直接看 DisplayMetrics 里面的属性。
/**
* The absolute width of the available display size in pixels.
*/
public int widthPixels;
/**
* The absolute height of the available display size in pixels.
*/
public int heightPixels;
这是就很清晰了吧,有了缩放系数,我们就可以精准的将 1dp 转换为 3.375px 了,达到完美适配。
2. 运行时获取到设备精准的 scaledDensity
得到 newDensity 以后,同样也要获取 newScaledDensity 的值,才能做到对字体 sp 的适配。
float newScaledDensity = newDensity * (dm.density / dm.scaledDensity);
这个还好理解的吧,因为默认状态 density 和 scaledDensity 是相等的,在修改过系统字体大小后,scaledDensity 会进行缩放,所以要将该比例考虑进去。
3. 计算 densityDpi
得到了缩放系数,在设置到 DisplayMetrics 之前,千万不要忘记对 densityDpi 也要进行计算,如果不修改 densityDpi 的话,程序内部计算会出现问题,因为源码中:
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
所以我们要反向计算出 densityDpi 的值:
int newDensityDpi = (int) (targetDensity * DisplayMetrics.DENSITY_DEFAULT);
4. 设置 DisplayMetrics 参数
终于到了最后一步,修改属性值:
dm.density = newDensity;
dm.scaledDensity = newScaleDensity;
dm.densityDpi = newDensityDpi;
这样,就可以使用 dp 值完美的适配各种屏幕了。