It's our wits that make us men.

← 源码笔记
Android 应用适配完全指南:分辨率与版本兼容

Android 应用适配完全指南:分辨率与版本兼容

一、基础概念

1.1 px、dp、sp 是什么

单位 全称 含义
px Pixel 物理像素,屏幕上的一个实际发光点
dp Density-independent Pixel 密度无关像素,用于布局尺寸
sp Scale-independent Pixel 缩放无关像素,用于文字大小(会跟随系统字体设置缩放)

为什么不能直接用 px?

同样写 100px,在不同屏幕上物理大小完全不同:

低密度屏 (160dpi):100px ≈ 1.59cm
高密度屏 (480dpi):100px ≈ 0.53cm   ← 缩小了 3 倍!

同样的像素数,高密度屏上看起来小得多。

dp 的设计思路

Android 把 160dpi 定为基准密度(density = 1),换算公式:

px = dp × (dpi / 160)
px = dp × density

设定按钮宽度为 100dp

160dpi 手机 → density=1 → 100×1 = 100px → 物理宽度 ≈ 1.59cm
320dpi 手机 → density=2 → 100×2 = 200px → 物理宽度 ≈ 1.59cm
480dpi 手机 → density=3 → 100×3 = 300px → 物理宽度 ≈ 1.59cm

像素数不同,但物理尺寸相同 — 这就是”密度无关”的含义。

dp 和 sp 的区别

dp → 只跟屏幕密度有关
sp → 跟屏幕密度 + 用户字体设置都有关
用户把系统字体调到 1.5 倍:
  16dp 的文字 → 还是 16dp 对应的像素  (不变)
  16sp 的文字 → 变成 24dp 对应的像素  (放大了)

使用规则:

1.2 像素密度与分辨率

分辨率 (Resolution)

屏幕上总共有多少个像素点,用 宽 × 高 表示:

1080 × 1920  → 横向 1080 个像素,纵向 1920 个像素
1440 × 3200  → 横向 1440 个像素,纵向 3200 个像素

分辨率只告诉你像素的数量,不告诉你屏幕的大小

像素密度 (PPI / DPI)

每英寸能塞下多少个像素点,衡量像素的紧密程度

PPI = Pixels Per Inch(每英寸像素数)
DPI = Dots Per Inch(每英寸点数,Android 中与 PPI 等价)

计算公式:

                √(宽像素² + 高像素²)
像素密度 PPI = ─────────────────────
                  屏幕对角线英寸数

关键区别

两块屏幕,分辨率相同,但大小不同

手机:  1080×1920,5.5 英寸  → PPI ≈ 401(密集,清晰)
平板:  1080×1920,10.1 英寸 → PPI ≈ 218(稀疏,颗粒感)
┌──────────┐          ┌─────────────────────┐
│ :::::::::: │          │ :  :  :  :  :  :  :  │
│ :::::::::: │          │ :  :  :  :  :  :  :  │
│ :::::::::: │  5.5寸   │ :  :  :  :  :  :  :  │  10.1寸
│ :::::::::: │  401ppi  │ :  :  :  :  :  :  :  │  218ppi
│ :::::::::: │          │ :  :  :  :  :  :  :  │
└──────────┘          └─────────────────────┘
  像素点密集               像素点稀疏

三者的关系

分辨率(像素数量)
─────────────── = 像素密度(PPI)
屏幕物理尺寸
  分辨率 像素密度 屏幕尺寸
描述 总共多少像素 像素有多密 屏幕多大
类比 一块地里种了多少棵树 树种得多密 地有多大
单位 px × px PPI (dpi) 英寸

知道任意两个,就能算出第三个。

常见密度等级

密度等级 dpi density 1dp = ?px 代表设备
mdpi 160 1.0 1px 早期低端机
hdpi 240 1.5 1.5px  
xhdpi 320 2.0 2px 主流中端机
xxhdpi 480 3.0 3px 主流旗舰
xxxhdpi 640 4.0 4px 高端旗舰

二、不同分辨率设备适配

2.1 多套资源目录

res/
├── layout/            # 默认布局
├── layout-sw360dp/    # 最小宽度 360dp
├── layout-sw600dp/    # 平板 7寸
├── layout-sw720dp/    # 平板 10寸
├── drawable-mdpi/     # 160dpi
├── drawable-hdpi/     # 240dpi
├── drawable-xhdpi/    # 320dpi
├── drawable-xxhdpi/   # 480dpi
├── drawable-xxxhdpi/  # 640dpi
└── values-sw600dp/    # 平板尺寸值

2.2 布局方案

val columns = (screenWidthDp / 180).coerceAtLeast(2)
recyclerView.layoutManager = GridLayoutManager(context, columns)

2.3 图片适配

2.4 今日头条适配方案(按设计稿等比缩放)

// 核心思路:修改 density 使屏幕宽度固定为设计稿宽度(如 360dp)
fun adaptScreen(activity: Activity, designWidthDp: Int = 360) {
    val appMetrics = activity.resources.displayMetrics
    val targetDensity = appMetrics.widthPixels.toFloat() / designWidthDp
    val targetDensityDpi = (targetDensity * 160).toInt()
    appMetrics.density = targetDensity
    appMetrics.scaledDensity = targetDensity * (appMetrics.scaledDensity / appMetrics.density)
    appMetrics.densityDpi = targetDensityDpi
}

三、不同 Android 版本适配

3.1 编译配置

android {
    compileSdk 35        // 编译用最新
    defaultConfig {
        minSdk 24        // 最低支持 Android 7.0
        targetSdk 35     // 目标版本
    }
}

3.2 运行时版本判断

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    // Android 13+ 专属逻辑
} else {
    // 兼容逻辑
}

3.3 AndroidX / Jetpack 兼容库

问题 兼容方案
通知 NotificationCompat
权限 ActivityResultContracts.RequestPermission
Fragment androidx.fragment
窗口适配 WindowCompat / WindowInsetsCompat
后台限制 WorkManager 替代 Service

3.4 各版本重点适配项

Android 8  (26) — 通知 Channel 必须创建
Android 10 (29) — 分区存储(Scoped Storage)
Android 11 (30) — 包可见性(package visibility), MANAGE_EXTERNAL_STORAGE
Android 12 (31) — 精确闹钟权限, PendingIntent 必须声明 mutability
Android 13 (33) — 通知运行时权限 POST_NOTIFICATIONS, 细化媒体权限
Android 14 (34) — 前台服务类型必须声明, 隐式 Intent 限制
Android 15 (35) — Edge-to-edge 强制全屏, 16KB page size 对齐

3.5 权限适配示例(通知权限)

val launcher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) { /* 发通知 */ }
}

fun requestNotification() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
    } else {
        // 13 以下默认有权限,直接发
    }
}

3.6 存储适配示例

fun saveImage(context: Context, bitmap: Bitmap) {
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "photo_${System.currentTimeMillis()}.jpg")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
        }
    }
    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
    )
    uri?.let { context.contentResolver.openOutputStream(it)?.use { os ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os)
    }}
}

四、跨版本 UI 显示兼容

4.1 核心框架选择

方案 兼容性 适用场景
Jetpack Compose 不依赖系统版本,UI 一致性最好 新项目首选
AndroidX AppCompat Material 控件向下兼容 传统 View 体系
原生 View 随系统版本变化大 不推荐
// Activity 继承 AppCompatActivity,而非 Activity
class MainActivity : AppCompatActivity() { ... }
<!-- 主题用 MaterialComponents / Material3 -->
<style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">

4.2 Edge-to-Edge(全面屏适配)

Android 15 强制 edge-to-edge,内容会延伸到状态栏和导航栏下方。

WindowCompat.setDecorFitsSystemWindows(window, false)

ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
    val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
    insets
}

4.3 刘海屏 / 挖孔屏(Android 9+)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    window.attributes.layoutInDisplayCutoutMode =
        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    val cutout = insets.displayCutout
    cutout?.let {
        val topSafe = it.safeInsetTop
        // 避免内容被刘海遮挡
    }
    insets
}

4.4 深色模式(Android 10+)

// 统一用 AppCompatDelegate 管理,兼容到 Android 5
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
<!-- 提供两套颜色资源 -->
res/values/colors.xml           <!-- 亮色 -->
res/values-night/colors.xml     <!-- 暗色 -->

4.5 圆角屏幕(Android 12+)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    view.setOnApplyWindowInsetsListener { v, insets ->
        val radius = insets.roundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius ?: 0
        // 根据圆角半径调整布局边距
        insets
    }
}

4.6 字体缩放(Android 14 非线性缩放)

// Android 14+ 字体缩放可达 200%,且是非线性的
// 使用 sp 单位会自动适配,但要注意布局溢出
// 关键:不要给文本容器设固定高度
<!-- 错误 -->
<TextView
    android:layout_height="48dp"
    android:textSize="16sp" />

<!-- 正确 -->
<TextView
    android:layout_height="wrap_content"
    android:minHeight="48dp"
    android:textSize="16sp" />

4.7 屏幕尺寸获取兼容工具

object CompatUtils {
    fun setStatusBarColor(window: Window, color: Int) {
        WindowInsetsControllerCompat(window, window.decorView).apply {
            isAppearanceLightStatusBars = ColorUtils.calculateLuminance(color) > 0.5
        }
        window.statusBarColor = color
    }

    fun getScreenSize(activity: Activity): Pair<Int, Int> {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val bounds = activity.windowManager.currentWindowMetrics.bounds
            bounds.width() to bounds.height()
        } else {
            val dm = DisplayMetrics()
            @Suppress("DEPRECATION")
            activity.windowManager.defaultDisplay.getMetrics(dm)
            dm.widthPixels to dm.heightPixels
        }
    }
}

五、测试验证

# 重点测试版本(行为变化最大):
API 26 (8.0)   — 通知 Channel、自适应图标
API 29 (10)    — 深色模式、手势导航
API 31 (12)    — Material You、动态颜色
API 34 (14)    — 预测性返回动画
API 35 (15)    — 强制 Edge-to-Edge

六、总结

维度 核心策略
分辨率 dp/sp + ConstraintLayout + 多套资源 + sw 限定符
版本 compileSdk 最新 + Build.VERSION.SDK_INT 判断 + AndroidX 兼容库
UI 显示 AppCompat/Compose + WindowInsetsCompat + Material3 主题

一句话:布局用约束不用固定值,API 用 Jetpack 不用原生旧接口。

← 返回目录