还在为 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,让所有手机的"屏幕宽度"等于设计稿宽度,页面代码无需改动,自动适配所有机型。

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

相关推荐
Kapaseker4 分钟前
为什么 Java 的数组需要 new 出来
android·java·kotlin
黄林晴19 分钟前
颠覆开发!Google AI Studio 一句话生成原生 Android App
android·google io
恋猫de小郭31 分钟前
Flutter 3.44 发布啦,超级大版本更新!!!
android·flutter·ios
zb2006412037 分钟前
Laravel10.x重磅升级:新特性全解析
android
2601_9574188044 分钟前
深入解析Android相机有线连接:PTP与MTP协议栈实现原理与实践
android·数码相机·智能手机
努力努力再努力wz1 小时前
【QT入门系列】QWidget 六大常用属性详解:windowOpacity、cursor、font、focus、toolTip 与 styleSheet
android·开发语言·数据结构·c++·qt·mysql·算法
撩得Android一次心动1 小时前
C语言基础笔记3【个人用】
android·c语言·开发语言·笔记
小离a_a1 小时前
uniapp小程序封装圆环显示比例数据
android·小程序·uni-app
三少爷的鞋1 小时前
Android 面试系列:runBlocking 到底该在哪用?
android
DogDaoDao9 小时前
Android 硬件编码器参数完全指南:MediaCodec 深度解析
android·音视频·视频编解码·h264·硬编码·视频直播·mediacodec