核心目标:让 UI 元素在不同屏幕上看起来物理尺寸差不多大。
想象一下:你在一个 5 英寸的小手机屏幕上画了一个 100px 宽的按钮,看起来大小合适。现在把这个按钮原封不动地显示在一个 10 英寸的大平板屏幕上。虽然平板的分辨率可能更高(比如有更多的像素),但 100px 宽的按钮在巨大的屏幕上会显得非常小 ,用户可能都点不准!反之,如果在平板上画一个 300px 宽的按钮,放到小手机上可能就占满整个屏幕宽度了。
这就是 px
(像素) 的局限性:它只代表屏幕上的一个发光点,不关心屏幕的实际物理尺寸和像素密度。为了解决这个问题,Android 引入了与物理尺寸挂钩的 dp
和衡量屏幕像素密度的 dpi
。
1. 概念详解
-
px
(Pixel - 像素):-
是什么? 屏幕显示的最小物理单位。就是屏幕上一个个会发光的小点。每个像素点显示一种颜色,无数个像素点组成了你看到的图像。
-
特点: 绝对的、物理的。100px 宽度的东西在任何屏幕上都是由 100 个发光点横向排列组成的。
-
问题: 不同设备的屏幕尺寸(英寸)和分辨率(总像素数 px * px)差异巨大。同样 100px 的按钮:
- 在一个小尺寸、高分辨率 (高
dpi
)的屏幕上,这 100 个发光点挤在很小的物理空间里,按钮看起来就小。 - 在一个大尺寸、低分辨率 (低
dpi
)的屏幕上,这 100 个发光点分布在大一点的物理空间里,按钮看起来就大。
- 在一个小尺寸、高分辨率 (高
-
结论: 尽量避免直接在布局或代码中使用
px
来定义尺寸! 因为它无法保证在不同设备上具有一致的物理尺寸观感。
-
-
dpi
(Dots Per Inch - 每英寸点数):-
是什么? 衡量屏幕物理像素密度 的指标。表示在一条 1 英寸(约 2.54 厘米) 长的直线上,能排列多少个像素点 (
px
)。 -
计算:
dpi = √(水平像素数² + 垂直像素数²) / 屏幕对角线尺寸(英寸)
- 简单理解:屏幕对角线上的总像素数除以屏幕对角线的物理长度(英寸)。
-
分类 (Android 的密度桶 - Density Bucket): 为了方便适配,Android 将不同
dpi
范围的屏幕划分成几个标准密度等级:ldpi
(low): ~120dpi (已罕见)mdpi
(medium): ~160dpi (这是 Android 的基准密度!)hdpi
(high): ~240dpixhdpi
(extra high): ~320dpixxhdpi
(extra extra high): ~480dpixxxhdpi
(extra extra extra high): ~640dpi
-
作用:
dpi
是系统判断屏幕有多"细腻"的关键参数。系统根据设备的实际物理dpi
将其归入上述某个密度桶。这直接影响dp
到px
的转换比例。
-
-
dp
或dip
(Density-independent Pixel - 密度无关像素):-
是什么? Android 设计的虚拟像素单位 。专门用来解决
px
在不同密度屏幕上物理尺寸不一致的问题。 -
核心思想: 以
mdpi
(160dpi) 屏幕作为基准。-
在 160dpi (
mdpi
) 的屏幕上:1dp = 1px。 -
在 320dpi (
xhdpi
) 的屏幕上(像素密度是mdpi
的 320/160 = 2倍):1dp = 2px 。系统会自动将你定义的dp
值乘以 2 转换成px
来渲染。这样,一个 100dp 宽的按钮:- 在
mdpi
屏上占用 100px 物理宽度。 - 在
xhdpi
屏上占用 200px 物理宽度。
- 在
-
因为
xhdpi
屏幕的像素更小更密集,200px 在xhdpi
屏幕上占据的物理宽度 (英寸)与 100px 在mdpi
屏幕上占据的物理宽度 是基本相等的!
-
-
转换公式:
px = dp * (dpi / 160)
或者更常见的是使用系统提供的缩放因子
density
:
px = dp * density
- 其中
density = dpi / 160f
- 例如:在
xhdpi
(320dpi) 设备上,density = 320 / 160 = 2.0
。那么 100dp * 2.0 = 200px。
- 其中
-
特点: 使用
dp
定义尺寸,能保证 UI 元素(按钮、文字框、图片框等)在不同密度 (dpi
) 的屏幕 上,物理尺寸(英寸/厘米)大致相同。用户感觉这个按钮在手机上和平板上"实际摸起来"应该差不多大。 -
使用: 在 XML 布局文件 (
layout.xml
) 和 代码中定义 View 的尺寸(宽、高、边距、内边距等)时,优先使用dp
。-
XML:
android:layout_width="100dp"
,android:padding="16dp"
-
代码 (Java/Kotlin):
scss// 设置宽度为 100dp int widthInPx = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics() ); myView.getLayoutParams().width = widthInPx; // Kotlin 更简洁 (使用扩展属性) val widthInPx = 100.dpToPx() // 需要自己写扩展函数或使用库 // 或者 val widthInPx = (100 * resources.displayMetrics.density).toInt()
-
-
-
sp
(Scale-independent Pixel - 缩放无关像素):- 是什么? 基于
dp
的单位,但额外考虑了用户的字体大小偏好设置(在系统设置 -> 显示 -> 字体大小 中调整)。 - 原理: 默认情况下,
1sp ≈ 1dp
。但当用户在系统设置中调大字体时,系统会将sp
值乘以一个缩放因子(如 1.1, 1.2 等)再转换成px
。 - 使用: 专门用于定义文字大小 (
TextView
的android:textSize
)。 这样用户放大系统字体时,你应用里的文字也会跟着变大,保证可读性。 - 注意: 对于非文字的尺寸(如图标大小、布局尺寸),仍然应该使用
dp
。否则用户调大字体,你的按钮也跟着变巨大就不好了。
- 是什么? 基于
2. 原理总结
-
物理现实: 屏幕有物理尺寸(英寸)和总像素数(分辨率)。
dpi
描述了每英寸挤了多少个像素 (px
)。 -
px
的问题: 同样数量的px
,在高dpi
(细腻) 屏上物理尺寸小,在低dpi
(粗糙) 屏上物理尺寸大。 -
dp
的解决方案:- 以
mdpi
(160dpi) 作为基准:1dp
在160dpi
屏上= 1px
。 - 转换规则:
px = dp * (dpi / 160)
。系统在运行时根据当前屏幕的实际dpi
(或其所属的密度桶) 自动计算这个比例。 - 最终效果:
100dp
的元素,无论在mdpi
(160dpi) 屏占用 100px,还是在xhdpi
(320dpi) 屏占用 200px,它们在用户手中看起来的物理大小(比如都是 1.5 厘米宽)是近似相等的。
- 以
-
sp
的延伸: 在dp
的基础上,再乘以用户设置的字体缩放比例,专门用于文字,保证用户可调。
3. 源码调用浅析 (简化版)
系统如何实现 dp
-> px
的转换?核心在 DisplayMetrics
类和 TypedValue
类。
-
DisplayMetrics
类 (android.util.DisplayMetrics):-
这个类封装了当前显示设备(屏幕)的各种物理和逻辑属性。
-
关键字段:
densityDpi
: 当前屏幕的物理 dpi 值(如 320)。density
: 缩放因子 。这是核心!density = densityDpi / 160f
。在mdpi
屏上是1.0
,在xhdpi
屏上是2.0
。scaledDensity
: 类似于density
,但会乘上用户设置的字体缩放比例 。主要用于sp
->px
的转换。
-
你的应用启动时,系统会根据当前运行的设备初始化一个
DisplayMetrics
实例(可通过Resources.getDisplayMetrics()
或Context.getResources().getDisplayMetrics()
获取)。
-
-
转换过程 (以
dp
->px
为例):-
XML 布局加载时: 当系统解析布局文件遇到
android:layout_width="100dp"
时,布局解析器 (如LayoutInflater
) 内部会调用类似TypedValue
的方法进行单位转换。 -
核心方法 -
TypedValue.applyDimension()
(android.util.TypedValue):arduinopublic static float applyDimension(int unit, float value, DisplayMetrics metrics) { switch (unit) { case COMPLEX_UNIT_PX: // px 直接返回 return value; case COMPLEX_UNIT_DIP: // dp 转换: value * metrics.density return value * metrics.density; case COMPLEX_UNIT_SP: // sp 转换: value * metrics.scaledDensity return value * metrics.scaledDensity; ... // 其他单位 (pt, in, mm) } return 0; }
-
代码中使用: 当你像前面的例子那样在代码中调用
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, metrics)
时:unit
参数指定源单位是dp
(COMPLEX_UNIT_DIP
)。value
参数是值 (100
)。metrics
参数是当前屏幕的DisplayMetrics
对象 (包含density=2.0
)。- 方法内部执行
100 * metrics.density = 100 * 2.0 = 200.0f
(px)。 - 你拿到这个
200px
的值,就可以设置给 View 的宽高了。
-
资源目录选择: 当系统需要加载图片资源时(如
@drawable/icon
),它会根据设备的密度桶 (mdpi
,hdpi
,xhdpi
...) 自动选择 相应目录下的资源(res/drawable-mdpi/
,res/drawable-hdpi/
,res/drawable-xhdpi/
...)。为不同密度提供不同分辨率的图片,也是为了最终在物理尺寸上显示一致,同时避免系统缩放导致模糊或浪费内存。
-
关键结论与最佳实践
-
理解概念:
px
是物理点,dpi
是屏幕密度,dp/sp
是虚拟单位用于适配。 -
使用原则:
- 布局尺寸 (宽、高、margin、padding): 始终使用
dp
。 - 文字大小: 始终使用
sp
。 - 避免使用
px
: 除非你有非常特殊且明确的需求(例如绘制一个精确到像素的细线),否则不要用px
。
- 布局尺寸 (宽、高、margin、padding): 始终使用
-
图片资源: 为不同的密度桶 (
mdpi
,hdpi
,xhdpi
,xxhdpi
) 提供相应分辨率的切图。系统会自动选择最合适的图片加载并缩放到合适的物理尺寸(以dp
定义的 ImageView 大小为准)。这能保证图片清晰度和物理尺寸一致性。 -
测试: 在尽可能多的不同屏幕尺寸和密度的设备(或模拟器/云测平台)上测试你的 UI 布局效果。
-
drawable
(无后缀)文件夹-
存放与密度无关的资源:
-
矢量图(
vector drawable
,格式为.xml
)。 -
颜色状态列表(
color state list
)。 -
形状定义(
shape
、selector
等)。 -
注意 :如果放位图(如
.png
)在这里,系统会默认按mdpi
处理,可能在高密度设备上显得模糊。
-
-
矢量图(Vector Drawable)
- 使用
res/drawable/icon.xml
(无需多密度版本)。 - 优点:无限缩放,无需提供多套位图。
- 限制:复杂图形可能性能较差(适合简单图标)。
- 使用
-
只提供高密度图片可以吗?
-
可以,但不推荐:
-
如果只提供
xxhdpi
图片,在mdpi
设备上系统会缩小它,可能浪费内存。 -
如果只提供
mdpi
图片,在xxhdpi
设备上放大会导致模糊。
-