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 的缓冲区尺寸,适配流程分为三步:

  1. 获取物理屏幕 DisplayMetrics:从 WindowManager 的默认 Display 获取,而非 Resources 的(可能受窗口装饰影响)。
  2. 计算逻辑宽高(dp):widthPixels / density,但需注意 cutout、status bar 区域;Surface 通常使用全屏尺寸。
  3. 建立 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 尺寸映射关系流程图

Surface尺寸与Resolution适配映射流 物理屏幕 宽×高(像素) 物理尺寸 & PPI xdpi / ydpi DisplayMetrics densityDpi, density 密度等级离散 160/240/320/480/640 逻辑尺寸 (dp) widthDp, heightDp Surface缓冲区设置 setFixedSize(realPx) 📌 关键点:Surface 缓冲区直接使用物理像素(px),UI 控件使用逻辑 dp。density 是连接两者的桥梁。 务必通过 WindowManager 获取 realMetrics,避免窗口装饰干扰。

4.8 部署与测试建议

  • 在覆盖所有密度等级的模拟器上验证:ldpi、mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi。
  • 测试横竖屏切换时 onConfigurationChanged 中 metrics 是否更新。
  • 使用 Android Studio 的 Layout Inspector 检查 Surface 的实际 buffer 尺寸是否匹配 getRealMetrics。

✅ 最佳实践:将 Density 转换工具封装为单例,在 Application 初始化时通过 context.getResources() 获取一次,但 Surface 适配时仍需从 WindowManager 重新获取真实尺寸。