Android适配完全指南
概述
Android设备具有各种屏幕尺寸和密度,为了确保应用在不同设备上都能良好显示,需要进行分辨率适配。本文将从基础概念、核心原理、实际应用等多个维度深入解析Android的适配机制。
一、屏幕密度基础概念
1. 什么是屏幕密度(dpi)?
dpi(Dots Per Inch)指"每英寸屏幕的像素点数",直接决定屏幕的清晰度:
- 同样大小的屏幕,dpi越高,像素越密集,显示越清晰
- Android为了统一适配标准,将「mdpi」定义为基准密度(160dpi),其他密度都是相对于mdpi的"比例值"
2. Android屏幕密度分类
Android系统将屏幕密度分为以下几个等级:
| 密度类别 | DPI范围 | 相对于mdpi的比例 | 常见设备举例 |
|---|---|---|---|
| mdpi | 120~160 | 1.0x(基准) | 早期低端手机 |
| hdpi | 160~240 | 1.5x | 早期中端手机 |
| xhdpi | 240~320 | 2.0x | 主流手机(如早期iPhone 8) |
| xxhdpi | 320~480 | 3.0x | 中高端手机(如iPhone 13) |
| xxxhdpi | 480~640 | 4.0x | 旗舰手机(如三星S23 Ultra) |
3. 密度计算公式
density = dpi / 160
二、Drawable资源适配详解
1. Drawable目录与dpi的对应规则
Android通过drawable目录的"密度后缀"(如-hdpi、-xhdpi),告诉系统"这个目录下的图片适合哪种dpi的设备"。
关键结论:每个drawable目录都绑定了一个"目标密度",图片放在哪个目录,系统就默认这张图是为该密度设备设计的。
2. 为什么必须把图片放到对应的密度目录?
答案很简单:平衡"显示清晰度"和"内存占用"——这是Android设计密度适配的核心目标。
以一张"100x100像素"的图片举例(假设是mdpi基准图),不同密度设备的理想图片尺寸和加载逻辑如下:
- mdpi设备(1.0x):需要100x100像素的图,直接加载drawable-mdpi的图,无需缩放,清晰且内存占用低(100×100×4字节=40KB,按ARGB8888计算)
- hdpi设备(1.5x):需要150x150像素的图(100×1.5),加载drawable-hdpi的图,无需缩放,清晰且内存占用合理(150×150×4=90KB)
- xhdpi设备(2.0x):需要200x200像素的图,加载drawable-xhdpi的图,无需缩放,清晰且内存占用可控(200×200×4=160KB)
如果我们不给每个密度目录放对应尺寸的图,系统就会"被迫缩放图片"——这就会引发问题。
3. 放错目录的具体问题
放错目录的本质是"图片尺寸与设备密度不匹配",系统会根据「设备密度/资源密度」计算缩放因子,强制拉伸/压缩图片,最终导致两类问题:模糊(像素化)或内存浪费(甚至两者并存)。
场景1:低分辨率图放到高密度目录(如mdpi图放xhdpi目录)
假设:把100x100像素的mdpi图,错误放到drawable-xhdpi目录,设备是xhdpi(2.0x)。
系统计算逻辑:
- 系统识别到图片在xhdpi目录,默认这张图是"为xhdpi设备设计的"(资源密度=2.0x)
- 设备实际密度是xhdpi(2.0x),计算缩放因子:缩放因子=设备密度/资源密度=2.0/2.0=1.0
- 系统会按1.0倍加载图片(即100x100像素),但xhdpi设备需要200x200像素的图才能填满屏幕——最终会把100x100的图拉伸到200x200
最终后果:
- 模糊(像素化):100x100的图被强行拉伸到200x200,像素点被"复制放大",画面出现锯齿、颗粒感
- 内存占用异常:拉伸后的图片像素数是200×200=40000,内存占用=40000×4=160KB(和正确加载xhdpi图的内存一样),但清晰度完全不如正确的图
场景2:高分辨率图放到低密度目录(如xhdpi图放mdpi目录)
假设:把200x200像素的xhdpi图,错误放到drawable-mdpi目录,设备是xhdpi(2.0x)。
系统计算逻辑:
- 系统识别到图片在mdpi目录,默认这张图是"为mdpi设备设计的"(资源密度=1.0x)
- 设备实际密度是xhdpi(2.0x),计算缩放因子:缩放因子=2.0/1.0=2.0
- 系统会按2.0倍加载图片——但这里有个误区:不是先加载200x200的图再放大,而是先计算"目标尺寸"(200×2.0=400x400),再把原图拉伸到400x400
最终后果:
- 严重模糊:200x200的图被拉伸到400x400,像素点被强行放大,画面模糊程度比场景1更严重
- 内存暴增:拉伸后的像素数是400×400=160000,内存占用=160000×4=640KB——是正确加载xhdpi图(160KB)的4倍,会导致内存紧张,甚至OOM(内存溢出)
场景3:图片放无后缀drawable目录(默认mdpi)
所有非mdpi设备加载时,都会按"设备密度/1.0x"的缩放因子拉伸图片,相当于场景1/2的"通用错误版":
- hdpi设备(1.5x):mdpi图被拉伸1.5倍,模糊+内存增加
- xxhdpi设备(3.0x):mdpi图被拉伸3倍,严重模糊+内存暴增(100×3=300像素,300×300×4=360KB,是正确xxhdpi图的4倍)
4. Android如何处理密度适配(源码解析)
要理解本质,必须看Android加载drawable资源的核心流程——关键逻辑在Resources、AssetManager、DisplayMetrics三个类中。
第一步:获取设备的屏幕密度(DisplayMetrics)
当App启动时,系统会通过DisplayMetrics记录当前设备的密度信息,核心参数:
- density:设备密度相对于mdpi的比例(如xhdpi是2.0f)
- densityDpi:设备实际dpi值(如xhdpi是320dpi)
- scaledDensity:带字体缩放的密度(适配系统字体大小,此处暂不关注)
代码获取方式(开发者可调用):
DisplayMetrics metrics = getResources().getDisplayMetrics();
float deviceDensity = metrics.density; // 如2.0f(xhdpi)
int deviceDpi = metrics.densityDpi; // 如320dpi(xhdpi)
第二步:查找匹配密度的drawable资源(AssetManager)
当调用getResources().getDrawable(R.drawable.ic_xxx)时,系统会通过AssetManager(资源管理器)查找资源,核心逻辑是"优先匹配设备密度的目录":
- AssetManager根据资源ID,先查找与设备密度完全匹配的目录(如xhdpi设备先找drawable-xhdpi)
- 如果找不到,会按"密度从高到低"查找(如xhdpi→hdpi→mdpi→ldpi),或"从低到高"(取决于系统版本,默认优先高密目录以保证清晰度)
- 找到资源后,会将资源的"密度信息"(如xhdpi对应2.0f)存入TypedValue对象
第三步:计算缩放因子并加载图片(Resources.loadDrawable)
找到资源后,Resources的loadDrawable方法会处理缩放,核心代码逻辑如下(简化版):
private Drawable loadDrawable(TypedValue value, int id, Theme theme) {
// 1. 获取设备的DisplayMetrics(密度信息)
DisplayMetrics metrics = getDisplayMetrics();
// 2. 获取资源的密度(value.density:如mdpi是160dpi,xhdpi是320dpi)
int resourceDpi = value.density;
if (resourceDpi == 0) {
// 无密度后缀的目录(drawable),默认mdpi(160dpi)
resourceDpi = DisplayMetrics.DENSITY_DEFAULT; // 160dpi
}
// 3. 计算缩放因子:设备密度/资源密度(注意单位转换:dpi→比例)
float scale = metrics.densityDpi / (float) resourceDpi;
// 4. 根据缩放因子,计算图片的目标尺寸
int targetWidth = (int) (originalWidth * scale);
int targetHeight = (int) (originalHeight * scale);
// 5. 加载图片:按目标尺寸缩放Bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = resourceDpi; // 资源的原始密度
options.inTargetDensity = metrics.densityDpi; // 设备的目标密度
Bitmap bitmap = BitmapFactory.decodeResourceStream(
getAssets(), value, is, null, options
);
// 6. 生成Drawable并返回
return new BitmapDrawable(metrics, bitmap);
}
源码关键结论:
- 缩放因子由「设备dpi/资源dpi」决定,而非"目录名称"(目录名称只是告诉系统资源的dpi)
- BitmapFactory会根据inDensity和inTargetDensity自动缩放图片,最终生成的Bitmap尺寸是「原始尺寸×缩放因子」
- 放错目录本质是"资源dpi与设备dpi不匹配",导致缩放因子≠1.0,触发强制缩放
5. 图片缩放计算公式
图片缩放比例 = 当前设备dpi / 图片文件夹dpi
示例说明
假设UI按照320dpi设计切图:
-
如果放到xhdpi目录下(xhdpi对应320dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/320dpi=1,图片宽高都不变
-
如果放到xxxhdpi目录下(xxxhdpi对应640dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/640dpi=0.5,图片宽高都变为原来的0.5倍,实际显示效果变小
-
如果放到mdpi目录下(mdpi对应160dpi): 运行在320dpi设备上时,图片缩放比例=320dpi/160dpi=2,图片宽高都变为原来的2倍,实际显示效果变大
6. 切图放置原则
UI按照什么dpi切图,就应该放到对应dpi的文件夹中,否则会出现图片实际显示效果偏大或偏小。
7. 图片适配查找顺序
系统按特定顺序查找图片资源:
以320dpi设备为例:
- 优先查找目标xhdpi
- 大于目标dpi从低到高:xxhdpi>xxxhdpi...
- 小于目标dpi,从高到低:hdpi>mdpi
因此320dpi设备完整查找顺序为:xhdpi->[xxhdpi->xxxhdpi]->[hdpi->mdpi]
对于213dpi设备:
因为213dpi与240dpi接近,所以会以hdpi作为目标文件夹
查找顺序为:hdpi->[xhdpi->xxhdpi->xxxhdpi]->[mdpi]
注意:如果按照213dpi设计切图并放到hdpi,图片缩放比例=213dpi/240dpi=0.8875,并不是1,所以目前的1.33倍切图不能放到hdpi。目标切图要按照标准dpi切图,这样才能正确适配。
8. 其他Drawable目录
drawable-nodpi: 不管设备什么dpi,都不缩放drawable-sw[xx]dp-hdpi: 限定最小尺寸xx的hdpi目录drawable-sw[xx]dp-xhdpi: 限定最小尺寸xx的xhdpi目录
三、Values资源适配详解
1. 实际像素计算公式
实际px = dp * (dpi / 160)
2. 标准Values目录
Values目录的查找顺序与图片查找顺序一致: 目标文件夹->[目标文件夹以上从低到高]->[目标文件夹以下从高到低]
例如,在213dpi设备上,由于213dpi与240dpi接近,所以会以values-hdpi作为目标文件夹:
values-hdpi->[values-xhdpi->values-xxhdpi->values-xxxhdpi]->[values-mdpi]
3. 宽度限定符(w)
计算公式
屏幕可用宽度 / (设备dpi / 160)
例如,分辨率为2560x1440,320dpi的设备: 对应的文件夹为2560/(320/160)=1280,即values-w1280dp
查找原则
如果同时存在以下目录:
- values-w719dp
- values-w720dp
- values-w721dp
如果目标w720dp中没有找到,则会向下查找最接近720的719,而不是向上的721
同样有高度限定符:values-h[xx]dp,但在实际开发中,一般都按照宽度适配,高度自适应。
宽高限定符:values-w[xx]dp-h[xx]dp
4. 最小宽度限定符(sw)
"Smallest Width"字面意思是最小宽度,实际上是指可用屏幕区域的最小尺寸,即屏幕可用宽度和高度中的较小值。
例如,2560x1440分辨率的设备,最小尺寸是1440(不是宽度2560)。
计算公式
最小尺寸 / (设备dpi / 160)
例如,2560x1440 320dpi的设备: 对应values-sw(1440/(320/160))dp=values-sw720dp
查找原则
与宽度限定符相同,如果找不到目标的,则向下查找最接近目标的。
如果同时存在以下目录:
- values-sw719dp
- values-sw720dp
- values-sw721dp
如果目标sw720dp中没有找到,则会向下查找最接近720的719,而不是向上的721
四、资源查找优先级
以2560x1440 320dpi为例:
目标文件夹优先级:
values-sw720dp>values-w1280dp>values-xhdpi
目标文件夹满足同级规则:
[values-sw720dp>values-sw719dp...]>
[values-w1280dp>values-w1279dp...]>
[values-xhdpi,[向上values-xxhdpi>values-xxxhdpi][向下values-hdpi>values-mdpi]]
五、Values目录vs图片目录
-
图片目录会参与缩放换算,需要严格按照目标切图dpi放置到对应的目录中。 例如,UI按照320dpi的切图就一定要放到xhdpi目录中。
-
Values目录不会参与缩放换算,任何values目录下的1dp=(dpi/160)px,所以不同的values目录要使用正确的dp值。
六、最佳实践建议
- 切图规范:严格按照标准dpi进行切图,并放置在对应的drawable目录中
- 多密度适配:为不同密度提供对应的图片资源,确保在各种设备上都有良好的显示效果
- 内存优化:避免将高分辨率图片放在低密度目录中,防止内存浪费
- 布局适配:使用dp单位进行布局,配合values-sw限定符实现屏幕适配
- 测试验证:在不同密度和尺寸的设备上测试应用显示效果
通过合理运用这些适配机制,可以确保Android应用在各种设备上都能提供一致且优质的用户体验。