4. Surface尺寸与分辨率适配:DisplayMetrics获取逻辑、density/dpi/densityDpi计算、屏幕真实尺寸与逻辑尺寸映射
Android 多屏幕碎片化严重,从 320×480 的小屏手机到 4K 平板、折叠屏、车载中控,Surface 的尺寸与分辨率适配必须精确理解 DisplayMetrics 的取值逻辑。本章从物理像素到逻辑 dp 的映射链路入手,详解 density、densityDpi 的计算方式,并给出适配 Surfaces 的正确做法。
4.1 原理:DisplayMetrics 与密度无关像素体系
Android 使用 dp(密度无关像素)作为 UI 尺寸单位。物理像素 px 与 dp 的换算公式:
px = dp × (density)
density = dpi / 160
DisplayMetrics 对象通过 Context.getResources().getDisplayMetrics() 获取,包含三个核心字段:
- densityDpi:屏幕密度等级(如 160、240、320、480、640),由系统按物理尺寸与分辨率综合计算。
- density:densityDpi / 160 的浮点值,用于 dp→px 转换。
- scaledDensity:用户字体缩放后的 density,用于 sp 单位。
设备真实物理尺寸(英寸)与显示尺寸不同,真实尺寸通过 DisplayMetrics.xdpi / ydpi 纵向/横向每英寸像素数推算:
物理宽(英寸) = widthPixels / xdpi
物理高(英寸) = heightPixels / ydpi
4.2 获取逻辑与真实尺寸映射流程
Surface 需要知道 SurfaceHolder 或 SurfaceView 的缓冲区尺寸,适配流程分为三步:
- 获取物理屏幕 DisplayMetrics:从
WindowManager的默认 Display 获取,而非Resources的(可能受窗口装饰影响)。 - 计算逻辑宽高(dp):
widthPixels / density,但需注意 cutout、status bar 区域;Surface 通常使用全屏尺寸。 - 建立 dp ↔ px 的双向映射表:用于设置 Surface 缓冲区大小与 UI 控件对齐。
| 密度等级 | densityDpi | density | 典型设备 |
|---|---|---|---|
| ldpi | 120 | 0.75 | 旧低端机 |
| mdpi | 160 | 1.0 | 基线 |
| hdpi | 240 | 1.5 | 中端手机 |
| xhdpi | 320 | 2.0 | 主流手机 |
| xxhdpi | 480 | 3.0 | 旗舰手机 |
| xxxhdpi | 640 | 4.0 | 平板/4K设备 |
⚠️ 注意:
densityDpi 是系统按经验值离散化的,实际物理 dpi 可能非整数。Surface 适配应使用 density 而非 dpi,因为 density 反映最终的缩放因子。
4.3 实现流程与核心代码
以下代码展示从 Activity 获取 DisplayMetrics 并计算逻辑尺寸,再设置 Surface 缓冲区大小:
// 1. 获取真实屏幕 DisplayMetrics(无窗口装饰干扰)
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
int realWidthPx = metrics.widthPixels;
int realHeightPx = metrics.heightPixels;
float density = metrics.density;
int densityDpi = metrics.densityDpi;
// 2. 计算逻辑 dp
int widthDp = (int) (realWidthPx / density);
int heightDp = (int) (realHeightPx / density);
// 3. 设置 Surface 缓冲区尺寸(通常使用物理像素)
SurfaceHolder holder = surfaceView.getHolder();
holder.setFixedSize(realWidthPx, realHeightPx);
// 4. 双向映射工具函数
public int dp2px(float dp) {
return (int) (dp * density + 0.5f);
}
public int px2dp(int px) {
return (int) (px / density);
}
⚠️ 常见陷阱:不要使用
Resources.getSystem().getDisplayMetrics(),它返回的是系统级默认 metrics,可能忽略屏幕旋转或多窗口模式。始终通过 WindowManager 的 current Display 获取。
4.4 调试与验证方法
打印关键指标到日志或 ADB 命令查看:
adb shell dumpsys display | grep "DisplayMetrics"
或者在代码中输出:
Log.d("SurfaceAdapter", "Real px: " + realWidthPx + "x" + realHeightPx);
Log.d("SurfaceAdapter", "Density: " + density + ", densityDpi: " + densityDpi);
Log.d("SurfaceAdapter", "Logical dp: " + widthDp + "x" + heightDp);
对比不同设备(模拟器可切换屏幕密度)验证映射是否正确。建议在 onSurfaceChanged 回调中再次确认 Canvas.getWidth()/getHeight() 与预期一致。
4.5 常见错误与避坑
- 错误使用 getResources().getDisplayMetrics():此方法不包含 cutout 区域,且在多窗口模式下返回窗口可用尺寸,可能导致 Surface 被裁剪。
- 忽略 scaledDensity 与 density 的区别:字体缩放应使用 scaledDensity,但 Surface 渲染与字体无关,依然用 density。
- 在非 UI 线程获取 DisplayMetrics:
WindowManager操作需在 UI 线程或使用Handler同步。 - 固定写死 density 值:例如假设密度为 3.0,实际设备可能是 2.0 或 3.5(某些定制ROM),应动态获取。
4.6 性能与稳定性优化
- 缓存 DisplayMetrics:避免重复创建对象,在 Activity 创建时获取一次,旋转等配置变化时在
onConfigurationChanged中更新。 - 使用硬件层缓冲区:对于大尺寸 Surface(如 4K 输出),确保
holder.setFormat(PixelFormat.RGBA_8888)与物理屏幕色深匹配。 - 避免频繁 setFixedSize:每次更改会重新分配 Surface 缓冲区,导致掉帧。目标尺寸应在 Surface 创建前确定。
4.7 尺寸映射关系流程图
4.8 部署与测试建议
- 在覆盖所有密度等级的模拟器上验证:ldpi、mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi。
- 测试横竖屏切换时
onConfigurationChanged中 metrics 是否更新。 - 使用 Android Studio 的 Layout Inspector 检查 Surface 的实际 buffer 尺寸是否匹配
getRealMetrics。
✅ 最佳实践:将 Density 转换工具封装为单例,在 Application 初始化时通过 context.getResources() 获取一次,但 Surface 适配时仍需从 WindowManager 重新获取真实尺寸。