DisplayMetrics
在Android开发中,
dpi(Dots Per Inch,每英寸的像素数)
是一个重要的概念,用于描述屏幕的像素密度。mdpi(Medium Density Pixel Image)是指每英寸有160像素点的屏幕。这个定义基于一个标准,即认为160dpi为基准密度,这意味着在mdpi屏幕上,1dp(设备无关像素)等于1px(像素)。
DisplayMetrics 是 Android 中用于描述设备显示屏的通用信息,如其大小、密度和缩放因子等。这是 Android 提供的一种帮助开发者适配不同屏幕尺寸和密度的工具。DisplayMetrics 主要包括以下重要字段和方法,用来获取屏幕的分辨率、密度、物理尺寸等信息。
- density: 屏幕的逻辑密度,基础密度值为 1.0(mdpi 的屏幕密度),该值是设备独立像素(dp)与实际像素之间的比例。
- densityDpi: 每英寸像素点数(dpi),是设备的屏幕密度,常见的屏幕密度有 mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 等。
- scaledDensity: 文字缩放密度,用来适配字体的缩放设置。
- heightPixels: 可用显示尺寸的高度(以像素为单位)。
- widthPixels: 可用显示尺寸的宽度(以像素为单位)。
- xdpi 和 ydpi: 屏幕在 X 和 Y 轴方向的物理像素密度。
kotlin
resources.displayMetrics.run {
log("density: $density")
log("densityDpi: $densityDpi")
log("scaledDensity: $scaledDensity")
log("widthPixels: $widthPixels")
log("heightPixels: $heightPixels")
log("xdpi: $xdpi")
log("ydpi: $ydpi")
}
手头的两台设备:
型号 | density | densityDpi | scaledDensity | heightPixels*widthPixels | ydpi*xdpi |
---|---|---|---|---|---|
小米11青春版 | 2.75 | 440 | 2.75 | 2400*1080 | 401.845*401.639 |
OPPO Reno Ace | 3.0 | 480 | 3.0 | 2400*1080 | 401.052*403.411 |
density和scaledDensity用途
density 和 scaledDensity通常用于dp、sp与px像素之间的转换,如:
kotlin
/**
* 将dp值转换为px值
*/
fun dp2px(context: Context, dp: Float): Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
fun px2dp(context: Context, px: Float): Int {
val scale = context.resources.displayMetrics.density
return (px / scale + 0.5f).toInt()
}
/**
* 将sp值转换为px值
*/
fun sp2px(context: Context, spValue: Float): Int {
val fontScale = context.resources.displayMetrics.scaledDensity
return (spValue * fontScale + 0.5f).toInt()
}
/**
* 将px值转换为sp值
*/
fun px2sp(context: Context, pxValue: Float): Int {
val fontScale = context.resources.displayMetrics.scaledDensity
return (pxValue / fontScale + 0.5f).toInt()
}
density和scaledDensity有何不同?
假设设备的屏幕密度为 xxhdpi(480 dpi),在这种情况下:
- density = 3.0 (这个值与设备的屏幕密度有关)
- 默认情况下,scaledDensity = density = 3.0 (当用户没有调整字体大小时)
但是,当用户调整了字体大小为更大的级别时,比如设置字体大小为 1.5倍,则:
- density 仍然是 3.0
- scaledDensity 变成 3.0 * 1.5 = 4.5
所以,density 用于 dp 转 px,只与屏幕密度相关;而scaledDensity 用于 sp 转 px,不仅与屏幕密度相关,还与用户设置的字体缩放相关,字体大小调整后,两者的取值可能会不同。
Drawable目录不同分辨率下的资源加载
不同分辨率文件夹中的图片,如 drawable-xxhdpi
、drawable-hdpi
、drawable-xhdpi
等,会影响解析出来的图片大小。Android 会根据设备的屏幕密度自动从不同的 drawable
文件夹中选择合适的资源文件。由于图片在不同分辨率文件夹中会有不同的尺寸,同一张图片在不同设备上解析的内存占用大小可能会不同。
Android 会根据设备的 屏幕密度(dpi) ,从相应的 drawable
文件夹中加载最适合当前设备的资源文件。常见的屏幕密度有:
- ldpi: 低分辨率 (~120 dpi)
- mdpi : 中分辨率 (~160 dpi),基准
- hdpi: 高分辨率 (~240 dpi)
- xhdpi: 超高分辨率 (~320 dpi)
- xxhdpi: 超超高分辨率 (~480 dpi)
- xxxhdpi: 超超超高分辨率 (~640 dpi)
对于同一个资源(例如 icon_launcher
),它在 drawable-xxhdpi
文件夹中可能是 144x144 的图像,而在 drawable-mdpi
中可能只有 48x48。解析时加载的图片大小不同,内存占用也不同。
示例代码
kotlin
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true //只解析图片元信息,不加载到内存
}
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
//图片占用内存大小 (bytes) = 图片宽度 (pixels) × 图片高度 (pixels) × 每个像素的字节数
val memorySize = options.outWidth * options.outHeight * getBytesPerPixel(options.inPreferredConfig)
log("图片宽度: ${options.outWidth}, 高度: ${options.outHeight}")
log("图片加载到内存时占用大小: ${memorySize / 1024} KB")
// 根据 Bitmap.Config 获取每个像素的字节数
private fun getBytesPerPixel(config: Bitmap.Config): Int {
return when (config) {
Bitmap.Config.ARGB_8888 -> 4 // 4字节(32位)
Bitmap.Config.RGB_565 -> 2 // 2字节(16位)
Bitmap.Config.ARGB_4444 -> 2 // 2字节(已被废弃)
Bitmap.Config.ALPHA_8 -> 1 // 1字节(8位,仅有透明度)
else -> 4 // 默认情况,按 ARGB_8888 计算
}
}
上述代码中,会通过 resources.displayMetrics.densityDpi
获取设备的屏幕密度。而密度的不同,会从不同的 drawable
文件夹中加载不同分辨率的图片,最终解析出的图片尺寸不同,导致内存占用不同。
假设设备是 xxhdpi(~480 dpi)的屏幕:
makefile
设备的屏幕密度: 480 dpi
图片宽度: 144, 高度: 144
图片加载到内存时占用大小: (144 x 144 x 4)/ 1024 = 81 KB
假设设备是 mdpi(~160 dpi)的屏幕:
makefile
设备的屏幕密度: 160 dpi
图片宽度: 48, 高度: 48
图片加载到内存时占用大小: (48 x 48 x 4)/ 1024 = 9 KB
上面的结论在手机是标准屏幕密度时是没问题的。让我们来换个设备,用上面提到的小米11手机再试试,该手机的屏幕密度densityDpi是440,并不是标准的480dpi,但是默认加载的依然是xxhdpi中的144x144
大小的图片: 如果按照上述公式计算依然是81KB,但是这个数据对吗?通过最终加载出来的bitmap来验证下:
kotlin
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = false //注意这里必须是false,否则decodeResource不会返回bitmap
}
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
log("图片大小 -> width:${bitmap.width}, height:${bitmap.height}, allocationByteCount:${bitmap.allocationByteCount}=${bitmap.allocationByteCount / 1024} KB")
执行结果:
arduino
图片大小 -> width:132, height:132, allocationByteCount:69696 = 68 KB
可以看到生成的bitmap,无论是宽高,还是内存大小都不是我们计算出来的结果,问题出在哪里呢?只能通过BitmapFactory.decodeResource源码方法来找答案了:
less
public static Bitmap decodeResource(Resources res, int id, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
//这里
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
}
//......
return bm;
}
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
//这个方法设置了opts.inDensity和opts.inTargetDensity
if (opts.inDensity == 0 && value != null) {
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 = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
上述流程中设置了opts.inDensity和opts.inTargetDensity ,然后通过decodeStream调用到Native层。
BitmapFactory.Options中:inDensity 是指图片资源的像素密度。inTargetDensity 是指当前设备的目标像素密度。如果两者不同,系统会根据这些密度信息缩放图片以适应屏幕分辨率。
直接看Native层的实现: 可以看到这里还有个scale系数,scale = (float) targetDensity / density
,生成bitmap时的宽高都会经过scale得到最终的宽高,所以最终公式如下:
arduino
图片所占内存:
= (width x scale) x (height x scale) x 每个像素所占字节数
= (width x targetDensity / density) x (height x targetDensity / density) x 每个像素所占字节数
对于小米11这款手机来说,targetDensity是440,density是480,代入公式计算一下:
ini
宽(width) = 高(height) = 144 x 440/480 = 132
所占内存 = 144 x 440/480 x144 x 440/480 x 4 / 1024 = 68KB
嗯 ,跟bitmap.allocationByteCount
返回的大小一样了,所以加载本地不同分辨率下的图片资源所占内存时还需要注意scale系数(targetDensity / density)。