还在为 Compose 屏幕适配发愁?一个 Density 搞定所有机型!

看完这篇文章,你将彻底理解 Compose 屏幕适配的本质,以后写 UI 再也不用担心在不同手机上显示效果不一致了。

一、先看问题

假设 UI 给你的设计稿是这样的:

你在代码里写了 width = 180.dp,然后测试在两台手机上运行:

手机 屏幕宽度 180dp占比
小米12 393dp 180/393=46%
红米k50 411dp 180/411=44%

问题:同样的 180dp,在不同手机上占比不一样!

原因:每台手机的「屏幕 dp 宽度」不同,但设计稿宽度是固定的。

二、解决思路:让所有手机的"屏幕宽度"变一样

核心思想(一句话) 让所有手机的"逻辑宽度"都等于设计稿宽度

怎么做?

通过修改 Density,让 dp → px 的转换比例按设计稿来算

三、先搞懂 Density 是什么

3.1 它是 dp 和 px 的"汇率"

把 Density 想象成汇率:

ini 复制代码
dp × Density = px

就像:

  • 100 美元 × 汇率 7.2 = 720 人民币
  • 100 dp × Density 3.0 = 300 px

3.2 系统 Density 怎么来的?

ini 复制代码
系统 Density = 屏幕像素宽度 / 屏幕dp宽度

举例:

手机 屏幕像素 屏幕dp Density
手机A 1080px 360dp 1080/360=3.0
手机B 1440px 400dp 1080/400=3.6

四、我们的方案:重新计算 Density

4.1 核心公式

ini 复制代码
新 Density = 屏幕实际像素宽度 / 设计稿宽度

4.2 举个例子

假设设计稿宽度是360dp:

手机 屏幕像素 新Density 效果
手机A 1080px 1080/360=3.0 屏幕逻辑宽度变成360dp
手机B 1440px 1440/360=4.0 屏幕逻辑宽度变成360dp

4.3 效果

修改后,所有手机的逻辑宽度都变成 360dp

那么 180dp 按钮:

  • 手机A:180/360 = 50%
  • 手机B:180/360 = 50%

比例完全一致!

五、先确认你的设计稿类型

不同设计团队的规范不同,先确认设计稿类型

设计稿类型 标注方式 如何换算
Android设计稿 直接标注dp值 直接使用标注值
iOS @2x 设计稿 标注像素值 标注值 ÷ 2
iOS @3x 设计稿 标注像素值 标注值 ÷ 3

如何确认?

  1. 问设计师:"标注的是 dp 还是像素?像素的话是几倍图?"
  2. 自己判断:
  • 如果标注值很大(如宽度 1080、1125),通常是像素值
  • 如果标注值正常(如宽度 360、375),通常是 dp 值

六、代码实现:集成到 Theme 中

6.1 创建适配 Theme

把屏幕适配逻辑放到 Theme 里,一次设置,全局生效:

ini 复制代码
/**
 * 自定义 Theme,集成屏幕适配
 */
@Composable
fun AppTheme(
    isDark: Boolean = isSystemInDarkTheme(),
    isTablet: Boolean = false,
    // 设计稿宽度,根据你的设计稿类型填写
    designWidthDp: Float = if (isTablet) 600f else 360f,
    designHeightDp: Float = if (isTablet) 960f else 640f,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    val configuration = LocalConfiguration.current

    // 获取屏幕像素宽度
    val screenWidthPx = context.resources.displayMetrics.widthPixels

    // 横竖屏判断
    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT

    // 选择设计稿基准:竖屏用宽度,横屏用高度
    val baseWidthDp = if (isPortrait) designWidthDp else designHeightDp

    // 核心:重新计算 Density
    val scaledDensity = Density(
        density = screenWidthPx / baseWidthDp,
        fontScale = LocalDensity.current.fontScale
    )

    // 设置颜色和字体
    val colorScheme = if (isDark) darkColorScheme() else lightColorScheme()

    // 注入新的 Density + MaterialTheme
    CompositionLocalProvider(LocalDensity provides scaledDensity) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = Typography(),
            content = content
        )
    }
}

6.2 使用方式

在 Activity 的 setContent 中包裹 AppTheme,后续所有页面自动适配:

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 只需包裹 Theme,内部所有页面自动适配
            AppTheme {
                MyAppNavigation()  // 你的导航/页面
            }
        }
    }
}

页面代码正常写,不需要任何额外包裹:

scss 复制代码
@Composable
fun HomePage() {
    // 直接用设计稿标注的 dp 值,自动适配
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "Hello World",
            fontSize = 16.sp
        )

        Button(
            modifier = Modifier
                .width(180.dp)
                .height(48.dp)
        ) {
            Text("按钮")
        }
    }
}

七、根据设计稿类型配置

在 Theme 中配置设计稿宽度即可:

scss 复制代码
// ===== Android 设计稿(直接标注 dp)=====
// 设计稿标注:宽度 360dp
AppTheme(designWidthDp = 360f) {
    MyApp()
}

// ===== iOS @2x 设计稿(标注像素,2倍图)=====
// 设计稿标注:宽度 720px
// 换算:720 / 2 = 360dp
AppTheme(designWidthDp = 720f / 2f) {
    MyApp()
}

// ===== iOS @3x 设计稿(标注像素,3倍图)=====
// 设计稿标注:宽度 1080px
// 换算:1080 / 3 = 360dp
AppTheme(designWidthDp = 1080f / 3f) {
    MyApp()
}

八、完整代码示例

8.1 AppTheme 完整版

ini 复制代码
/**
 * 应用主题,集成屏幕适配
 *
 * 使用方式:在 Activity setContent 中包裹即可
 *
 * @param isDark 是否深色模式
 * @param isTablet 是否平板
 * @param designWidthDp 竖屏设计稿宽度(dp)
 *                      - Android 设计稿:直接填标注值,如 360f
 *                      - iOS @2x 设计稿:标注值 / 2,如 720 / 2 = 360f
 *                      - iOS @3x 设计稿:标注值 / 3,如 1080 / 3 = 360f
 * @param designHeightDp 横屏设计稿宽度(通常是竖屏高度)
 */
@Composable
fun AppTheme(
    isDark: Boolean = isSystemInDarkTheme(),
    isTablet: Boolean = false,
    designWidthDp: Float = if (isTablet) 600f else 360f,
    designHeightDp: Float = if (isTablet) 960f else 640f,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    val configuration = LocalConfiguration.current

    // 获取屏幕像素宽度
    val screenWidthPx = context.resources.displayMetrics.widthPixels

    // 横竖屏判断:竖屏用宽度基准,横屏用高度基准
    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT
    val baseWidthDp = if (isPortrait) designWidthDp else designHeightDp

    // 核心:重新计算 Density,让逻辑宽度等于设计稿宽度
    val scaledDensity = Density(
        density = screenWidthPx / baseWidthDp,
        fontScale = LocalDensity.current.fontScale
    )

    // 颜色方案
    val colorScheme = if (isDark) {
        darkColorScheme(
            primary = Color(0xFF6200EE),
            secondary = Color(0xFF03DAC6)
        )
    } else {
        lightColorScheme(
            primary = Color(0xFF6200EE),
            secondary = Color(0xFF03DAC6)
        )
    }

    // 注入新的 Density + MaterialTheme
    CompositionLocalProvider(LocalDensity provides scaledDensity) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = Typography(),
            content = content
        )
    }
}

8.2 Activity 使用

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                NavHost(
                    startDestination = "home"
                ) {
                    composable("home") { HomePage() }
                    composable("detail") { DetailPage() }
                    // 其他页面...
                }
            }
        }
    }
}

// 页面代码正常写,自动适配
@Composable
fun HomePage() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("首页", fontSize = 24.sp)
        // 直接用设计稿 dp 值...
    }
}

@Composable
fun DetailPage() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("详情页", fontSize = 20.sp)
        // 直接用设计稿 dp 值...
    }
}

九、图解原理

适配前

适配后

一句话总结

在 Theme 中修改 Density,让所有手机的"屏幕宽度"等于设计稿宽度,页面代码无需改动,自动适配所有机型。

如果觉得有帮助,点个赞吧!有问题欢迎评论区讨论~

相关推荐
卡尔特斯2 小时前
Android Studio 代理配置指南
android·前端·android studio
sunbofiy232 小时前
去掉安卓的“读取已安装应用列表”,隐私合规
android
cch89182 小时前
DCATAdmin后台框架极速上手
android
Ehtan_Zheng3 小时前
ActivityMetricsLogger 深度剖析:系统如何追踪启动耗时
android
用户69371750013844 小时前
Android 开发,别只钻技术一亩三分地,也该学点“广度”了
android·前端·后端
唔664 小时前
原生 Android(Kotlin)仅串口「继承架构」完整案例二
android·开发语言·kotlin
一直都在5724 小时前
MySQL索引优化
android·数据库·mysql
代码s贝多芬的音符5 小时前
android mlkit 实现仰卧起坐和俯卧撑识别
android
jwn9996 小时前
Laravel9.x核心特性全解析
android