看完这篇文章,你将彻底理解 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 |
如何确认?
- 问设计师:"标注的是 dp 还是像素?像素的话是几倍图?"
- 自己判断:
- 如果标注值很大(如宽度 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,让所有手机的"屏幕宽度"等于设计稿宽度,页面代码无需改动,自动适配所有机型。
如果觉得有帮助,点个赞吧!有问题欢迎评论区讨论~