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

0 阅读5分钟

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

一、先看问题

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

企业微信截图_17756320878658.png

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

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

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

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

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

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

怎么做?

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

三、先搞懂 Density 是什么

3.1 它是 dp 和 px 的"汇率"

把 Density 想象成汇率:

dp × Density = px

就像:

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

3.2 系统 Density 怎么来的?

系统 Density = 屏幕像素宽度 / 屏幕dp宽度

举例:

手机屏幕像素屏幕dpDensity
手机A1080px360dp1080/360=3.0
手机B1440px400dp1080/400=3.6

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

4.1 核心公式

Density = 屏幕实际像素宽度 / 设计稿宽度

4.2 举个例子

假设设计稿宽度是360dp:

手机屏幕像素新Density效果
手机A1080px1080/360=3.0屏幕逻辑宽度变成360dp
手机B1440px1440/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 里,一次设置,全局生效:

/**
 * 自定义 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,后续所有页面自动适配:

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

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

@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 中配置设计稿宽度即可:

// ===== 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 完整版

/**
 * 应用主题,集成屏幕适配
 *
 * 使用方式:在 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 使用

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 值...
    }
}

九、图解原理

适配前

企业微信截图_17756345909863.png

适配后

企业微信截图_17756346425405.png

一句话总结

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

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