写在前面的话

只要做过 Android UI 相关的开发,就一定接触过 dp px sp dip dpi 等概念,不过我面试过好多候选人没有几个能回答出所有细节,有的候选人连基本概念都弄不清,更别说是细节了。其实关于 dp px 等概念是非常简单的,会不会并不能完全体现出一个人的技术水平和技术深度,但是如果你能系统的总结出 dp px sp dip dpi 之间的关系和细节,对于你平时学习总结能力绝对是个加分项。

px(pixel 像素)

px 也叫像素点,是屏幕物理上的最小显示单位,如手机分辨率 1080 x 1920 表示宽有 1080 像素点,高有 1920 像素点。像素的大小是没有固定长度的,不同设备上一个单位像素色块的大小是不一样的。尺寸面积大小相同的两块屏幕,分辨率大小可以是不一样的,分辨率高的屏幕上面像素点(色块)就多,所以屏幕内可以展示的画面就更细致,单个色块面积更小,而分辨率低的屏幕上像素点(色块)更少,单个像素面积更大,可以显示的画面就没那么细致。

dpi(Dots Per Inch)

而在引入 dp 之前,有必要先了解一下另一个概念:dpi。也有人将 dpi 称为屏幕密度,其实就是每英寸所打印的点数。怎么计算一个设备屏幕的 dpi,其实非常简单,举个例子:
假设现在有一台宽 2 英寸,长 3 英寸的设备,则:

  • 当该设备分辨率为 320 x 480,则 dpi 值为 160;
  • 当该设备分辨率为 640 x 960,则 dpi 值为 320;

怎么算的呢?反应快的同学可能看出规律了就是 320 除以 2,480 除以 3,但并不完全对,如果横向 dpi 和纵向 dpi 不相等怎么办呢?实际上有很多设备的横向 dpi 和 纵向 dpi 是不相等的,这时用勾股定理算出屏幕对角的像素数再除以屏幕对角的尺寸就是最终的 dpi 了,而且我们平时所说的屏幕尺寸是 5.2 寸 6.5 寸等都是指屏幕对角尺寸。
以小米 mix2 为例,分辨率是 1080 x 2160, 屏幕尺寸是 5.99 英寸,那么按照上述算法就是 1080 平方加上 2160 平方再开平方等于 2414.95,然后 2414.95 / 5.99 = 403 dpi
用 dpi 衡量屏幕显示的精细程度是最准确直观的,因为单纯看分辨率或者屏幕尺寸都是不完整的。

ppi(pixels per inch)

谈完 dpi 就可以说说 ppi 了,这两个措辞的差别,表面上看来只在于是在谈 dot,还是 pixel,但实际上 dot 可以打印的墨点,可以指扫描仪的采样点,可以指数字图像的最小单位(即 pixel),可以指屏幕的物理像素,可以指操作系统的抽象像素……在不同的语境下可以指不同的概念,而同样 pixel 也可以指数字图像的数据 pixel,可以指屏幕物理像素,也可以指代操作系统的抽象像素……在不同语境下的意义也不同。这两个单位完全就是时常混用的,在电子屏幕显示中提到的 ppi 和 dpi 可以认为是一样的,所以你可以忽略在措辞上用 dpi 或者 ppi 有什么不同,不过在 Android 平台上常用 dpi 这种表述方式。

dp / dip (device independent pixels)

介绍完 px 和 dpi,dp 的概念就比较好理解了,首先说明下 dp 和 dip 是一个概念,都是 device independent pixels 的简称,只是有时和 dpi 放一起,dp dpi dip 会让初学者有点晕。dp 翻译过来就是设备独立像素,设备独立像素是什么意思呢?先看下面这种场景:
在宽 2 英寸,长 3 英寸的屏幕上,如果将一个 ImageView 的宽度设为160 px,则会出现如下情况:
如果屏幕分辨率宽度为 320 像素,该 ImageView 显示占屏幕宽度一半;
如果屏幕分辨率宽度为 640 像素,该 ImageView 显示占屏幕宽度的四分之一;
这对用户的体验是非常不好的,不同分辨率的设备上给用户的感觉就是图片大小不一样,而且还有个出现显示不全和显示越界的情况。为了解决这种问题 Android 引入了设备独立像素的概念,也就是 dp 这种单位。那么 dp 如何解决不同设备显示大小不一的问题呢? 首先 Android 规定在 160 dpi 的设备上 1 dp = 1 px, 当屏幕密度为 320 dpi 时,则 1 dp = 2 px,以此类推…….也正是因此,让我们得以保证了控件在不同密度的屏幕上显示一致,既完成屏幕适配。让我们回到上面说到的使用 px 造成的控件显示问题的场景,此时我们将使用新的单位 dp,将一个 ImageView 的宽度设为160 dp,于是:
在分辨率为 320 x 480(既 dpi 为 160)的屏幕上,则 160dp 等价于 160px,ImageView 占屏幕宽的一半;
在分辨率为 640 x 960(既 dpi 为 320)的屏幕上,则 160dp 等价于 320px,ImageView 依然占屏幕宽的一半,就解决大小比例不一致的问题。下面是网上常用的 dp 和 px 之间转换的工具类:

public static int dp2px(Context context, int values) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (values * scale + 0.5f);
    }

    public static int px2dip(Context context, float pxValue) {  
        float scale = context.getResources().getDisplayMetrics().density;  
        return (int) (pxValue / scale + 0.5f);  
   }

+0.5 就是为了在精度丢失的情况下进行四舍五入。

drawable 目录加载和 dpi 间的关系

说到 dpi 这里就不得不联想到在 Android 工程里 res 目录下的 drawable-hdpi, drawable-xhdpi, drawable-xxhdpi 等文件夹。我们知道 Android 会根据屏幕的 dpi 去选择对应的 drawable 文件夹,详细的规则可以参考我的另一篇文章 Android 开发之关于 drawable 你必须知道的规则,这里我们只讨论和 dpi 相关的规则,Android项目的资源文件下存在以下目录:

  • drawable-ldpi ( 当dpi为120时,使用此目录下的资源)
  • drawable-mdpi ( 当dpi为160时,使用此目录下的资源)
  • drawable-hdpi ( 当dpi为240时,使用此目录下的资源)
  • drawable-xhdpi ( 当dpi为320时,使用此目录下的资源)
  • drawable-xxhdpi ( 当dpi为480时,使用此目录下的资源)
  • drawable-xxxhdpi ( 当dpi为640时,使用此目录下的资源)

Android 正是根据设备DPI值得不同,选择清晰度不同的资源使用,完成屏幕的适配, 可以看出系数比例关系:0.75:1:1.5:2:3:4,但手机屏幕千奇百怪,例如如果一个手机屏幕是 420 dpi 不属于上述文件分类中的任何一个,因此上述文件夹不是指定具体的分辨率,而是一个范围:

  • drawable-ldpi(value <= 120 dpi)
  • drawable-mdpi(120 dpi < value <= 160 dpi)
  • drawable-hdpi(160 dpi < value <= 240 dpi)
  • drawable-xhdpi(240 dpi < value <= 320 dpi)
  • drawable-xxhdpi(320 dpi < value <= 480 dpi)
  • drawable-xxxhdpi(480 dpi < value <= 640 dpi)

以此类推,所以 420 dpi 会优先加载 xxdpi 中的资源文件,如果对应 dpi 目录下没有找到该资源文件,遵循先高再低原则,然后按比例缩放图片,比如当前为 xhdpi 设备,则 drawable 的寻找顺序为:
xhdpi -> xxhdpi -> xxxhdpi (如果没有更高的了) -> nodpi (如果有的话) -> hdpi -> mdpi,如果在 xxhdpi 中找到目标图片,则压缩 2/3 来使用,如果在 mdpi 中找到图片,则放大 2 倍来使用。

sp(scalable pixels)

最后来说一说 sp,sp 与缩放无关的像素单位,类似 dp ,不同之处在于它还会根据用户字体大小配置而缩放。所以开发中指定字体大小时建议使用 sp ,因为 sp 作为字体大小单位会随着系统的字体大小改变,下面是 sp 和 px 之间转换工具类:

 public static int px2sp(Context context, float pxValue) {  
         float fontScale = context.getResources().getDisplayMetrics().scaledDensity;  
         return (int) (pxValue / fontScale + 0.5f);  
   }  

     public static int sp2px(Context context, float spValue) {  
         final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;  
          return (int) (spValue * fontScale + 0.5f);  
   }

从代码中也可以看出 sp 和 px 间转换使用的是 scaledDensity,而 dp 和 px 间转换使用的是 density,也就是 sp 会随着系统字体设置缩放,dp 不会。这里有官方解释 Understanding layout

总结

正如我开头所说的概念性的知识没有什么技术深度,但是如果你能系统的做出总结和对比,对于你平时学习总结能力绝对是个加分项。