1. DisplayMetrics基础解析
在Android开发中,DisplayMetrics类是我们获取设备屏幕信息的核心工具。这个位于android.util包下的类,包含了设备显示相关的关键参数,开发者可以通过它获取屏幕的物理尺寸、像素密度等重要信息。
1.1 核心字段详解
DisplayMetrics类中最常用的几个字段包括:
- widthPixels:屏幕的绝对宽度(以px为单位)
- heightPixels:屏幕的绝对高度(以px为单位)
- densityDpi:屏幕的密度DPI值(每英寸点数)
- density:密度比例因子(相对于基准密度160dpi)
- scaledDensity:考虑字体缩放后的密度比例因子
这些字段中,density和densityDpi的关系特别值得注意。density实际上是densityDpi/160的结果,这个比例因子在dp和px转换时非常关键。
1.2 获取DisplayMetrics实例
获取DisplayMetrics实例的标准方式是通过WindowManager:
DisplayMetrics metrics = new DisplayMetrics(); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getMetrics(metrics);在Android API 17+版本中,推荐使用getRealMetrics()替代getMetrics(),因为它会包含系统装饰(如状态栏)的尺寸:
windowManager.getDefaultDisplay().getRealMetrics(metrics);2. 屏幕分辨率获取实践
2.1 基础获取方法
获取屏幕分辨率最直接的方式就是使用DisplayMetrics的widthPixels和heightPixels:
val displayMetrics = resources.displayMetrics val screenWidth = displayMetrics.widthPixels val screenHeight = displayMetrics.heightPixels Log.d("ScreenInfo", "Resolution: ${screenWidth}x${screenHeight}")但需要注意,这种方法获取的是应用可用区域的尺寸,不包括系统装饰(如状态栏、导航栏)。
2.2 完整屏幕尺寸获取
要获取包含系统UI的完整屏幕尺寸,需要使用以下方法:
Point size = new Point(); windowManager.getDefaultDisplay().getRealSize(size); int width = size.x; int height = size.y;或者在API 17+上使用getRealMetrics():
val realMetrics = DisplayMetrics() windowManager.defaultDisplay.getRealMetrics(realMetrics) val realWidth = realMetrics.widthPixels val realHeight = realMetrics.heightPixels2.3 多窗口模式处理
在Android 7.0+的多窗口模式下,屏幕分辨率获取需要特殊处理:
val configuration = resources.configuration val screenWidthDp = configuration.screenWidthDp val screenHeightDp = configuration.screenHeightDp这种方式获取的是dp单位的尺寸,可以更好地适应不同屏幕密度。
3. 密度与单位转换
3.1 密度类型与适配
Android定义了多种屏幕密度类型:
| 密度类型 | DPI范围 | 比例因子 |
|---|---|---|
| ldpi | ~120dpi | 0.75x |
| mdpi | ~160dpi | 1x (基准) |
| hdpi | ~240dpi | 1.5x |
| xhdpi | ~320dpi | 2x |
| xxhdpi | ~480dpi | 3x |
| xxxhdpi | ~640dpi | 4x |
3.2 dp与px转换
在代码中进行dp和px转换的标准方法是:
// dp转px fun dpToPx(dp: Float): Int { return (dp * resources.displayMetrics.density).toInt() } // px转dp fun pxToDp(px: Float): Int { return (px / resources.displayMetrics.density).toInt() }更推荐使用系统提供的TypedValue.applyDimension()方法:
float px = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics() );3.3 sp与px转换
处理文字大小时,需要考虑用户的字体缩放偏好:
fun spToPx(sp: Float): Float { return sp * resources.displayMetrics.scaledDensity }4. 实际应用场景
4.1 响应式布局实现
利用屏幕分辨率信息可以实现响应式布局:
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="@{screenWidth > 600 ? 'horizontal' : 'vertical'}">或者在代码中动态调整:
if (displayMetrics.widthPixels > 600.dpToPx()) { // 宽屏布局 } else { // 窄屏布局 }4.2 图片适配策略
根据屏幕密度加载不同分辨率的图片:
val densityBucket = when { displayMetrics.densityDpi >= DisplayMetrics.DENSITY_XXXHIGH -> "xxxhdpi" displayMetrics.densityDpi >= DisplayMetrics.DENSITY_XXHIGH -> "xxhdpi" displayMetrics.densityDpi >= DisplayMetrics.DENSITY_XHIGH -> "xhdpi" displayMetrics.densityDpi >= DisplayMetrics.DENSITY_HIGH -> "hdpi" displayMetrics.densityDpi >= DisplayMetrics.DENSITY_MEDIUM -> "mdpi" else -> "ldpi" }4.3 游戏开发中的运用
在游戏开发中,通常需要根据屏幕分辨率设置视口:
fun setupViewport() { val aspectRatio = displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels if (aspectRatio > 1.77f) { // 超宽屏 // 特殊处理 } else { // 标准处理 } }5. 常见问题与解决方案
5.1 获取值不正确的情况
问题现象:在onCreate()中获取的尺寸可能不准确。
解决方案:在View的onSizeChanged()或onWindowFocusChanged()中获取:
override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { val metrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(metrics) // 现在可以获取准确尺寸 } }5.2 折叠屏设备适配
折叠屏设备需要监听屏幕变化:
val callback = object : DisplayManager.DisplayListener { override fun onDisplayChanged(displayId: Int) { updateLayout() } // 其他方法实现... } val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager displayManager.registerDisplayListener(callback, null)5.3 刘海屏/挖孔屏处理
获取安全区域尺寸:
val windowInsets = ViewCompat.getRootWindowInsets(window.decorView) val safeInsetLeft = windowInsets?.systemWindowInsetLeft ?: 0 val safeInsetRight = windowInsets?.systemWindowInsetRight ?: 0 val safeWidth = displayMetrics.widthPixels - safeInsetLeft - safeInsetRight6. 性能优化建议
6.1 避免频繁获取
DisplayMetrics的获取虽然不昂贵,但也不应在循环中频繁调用。最佳实践是在Activity创建时获取一次并缓存:
class MainActivity : AppCompatActivity() { private lateinit var displayMetrics: DisplayMetrics override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) displayMetrics = resources.displayMetrics // 后续使用缓存的实例 } }6.2 使用ViewConfiguration预定义值
对于常见的UI交互阈值,使用系统预定义值:
val touchSlop = ViewConfiguration.get(this).scaledTouchSlop val minimumFlingVelocity = ViewConfiguration.get(this).scaledMinimumFlingVelocity这些值已经根据屏幕密度进行了适当缩放。
6.3 矢量图形优先
尽可能使用矢量图形(VectorDrawable)替代位图:
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> </vector>7. 测试与验证
7.1 多设备测试策略
在Android Studio的模拟器中测试不同分辨率:
- 创建AVD时选择不同设备预设
- 特别测试极端比例设备(如超宽屏)
- 验证折叠屏的展开/折叠状态
7.2 自动化测试脚本
编写测试用例验证分辨率相关逻辑:
@RunWith(AndroidJUnit4::class) class DisplayMetricsTest { @Test fun testDpToPxConversion() { val context = InstrumentationRegistry.getInstrumentation().targetContext val expectedPx = (16 * context.resources.displayMetrics.density).toInt() assertEquals(expectedPx, dpToPx(context, 16f)) } }7.3 实际设备验证清单
在真机测试时检查以下项目:
- 横竖屏切换时的布局表现
- 多窗口模式下的尺寸变化
- 系统字体大小改变后的影响
- 高密度屏幕上的图像清晰度
8. 高级技巧与最佳实践
8.1 动态计算布局参数
根据屏幕尺寸动态计算布局参数:
fun calculateColumns(): Int { val displayMetrics = resources.displayMetrics val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density return maxOf(1, (screenWidthDp / 120).toInt()) // 每列至少120dp }8.2 处理屏幕方向变化
正确处理配置变化:
<activity android:name=".MainActivity" android:configChanges="orientation|screenSize|screenLayout">并在代码中处理变化:
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val newScreenWidthDp = newConfig.screenWidthDp // 更新布局 }8.3 支持超大字体
考虑用户可能设置了超大字体:
val config = resources.configuration val fontScale = config.fontScale val adjustedSize = baseSize * fontScale9. 兼容性处理
9.1 旧版本API适配
对于API 16及以下版本的特殊处理:
@Suppress("DEPRECATION") fun getRealScreenSize(context: Context): Point { val point = Point() val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { windowManager.defaultDisplay.getRealSize(point) } else { windowManager.defaultDisplay.getSize(point) } return point }9.2 特殊设备处理
某些设备可能有非标准实现:
fun getReliableScreenSize(context: Context): Point { return try { getRealScreenSize(context) } catch (e: Exception) { val metrics = context.resources.displayMetrics Point(metrics.widthPixels, metrics.heightPixels) } }10. 工具与调试
10.1 ADB命令查看
通过ADB获取设备屏幕信息:
adb shell wm size adb shell wm density10.2 开发辅助工具
创建屏幕信息显示工具类:
object ScreenUtils { fun logScreenInfo(context: Context) { val metrics = context.resources.displayMetrics val config = context.resources.configuration Log.d("ScreenInfo", """ Resolution: ${metrics.widthPixels}x${metrics.heightPixels} Density: ${metrics.densityDpi}dpi (${metrics.density}x) Screen size: ${config.screenWidthDp}dp x ${config.screenHeightDp}dp Orientation: ${if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) "Landscape" else "Portrait"} Font scale: ${config.fontScale}x """.trimIndent()) } }10.3 布局边界调试
在开发者选项中开启"显示布局边界",可以直观看到各视图的实际像素尺寸。