一、Compose 布局三剑客:Column、Row 与 Box

说实话,刚接触 Compose 的时候,我最不适应的是——怎么没有 XML 了?

但用了一周后,我彻底回不去了。Compose 的布局方式,说白了就是用代码“画”界面。而 Column、Row、Box 这三个容器,就是你的画笔。

1.1 Column:纵向排列

Column 相当于传统 Android 里的 LinearLayout 方向为 vertical。子元素从上往下排。

@Composable
fun MyColumn() {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("第一行")
        Text("第二行")
        Button(onClick = { }) { Text("点击") }
    }
}
我的经验:verticalArrangement 控制纵向间距,horizontalAlignment 控制横向对齐。我习惯把 SpaceEvenly 用在列表项均匀分布的场景,比如设置页。

1.2 Row:横向排列

Row 就是水平版的 Column。子元素从左往右排。

@Composable
fun MyRow() {
    Row(
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(Icons.Default.Home, contentDescription = null)
        Text("首页")
        Switch(checked = true, onCheckedChange = { })
    }
}

嗯,这里要注意:Row 的 horizontalArrangement 和 Column 的 verticalArrangement 是对应的。别搞混了。

1.3 Box:层叠布局

Box 相当于 FrameLayout。子元素可以叠在一起。我经常用它做“徽标角标”效果。

@Composable
fun BadgeBox() {
    Box(modifier = Modifier.size(48.dp)) {
        Icon(Icons.Default.Person, contentDescription = "用户", modifier = Modifier.fillMaxSize())
        Box(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .size(16.dp)
                .background(Color.Red, CircleShape)
        )
    }
}
避坑:我曾经在 Box 里用 Modifier.align 时忘了指定父容器的 size,结果子元素跑到了屏幕外面。记住:align 依赖父容器的尺寸约束。

二、ConstraintLayout 在 Compose 中的使用

你可能会问:有了 Column/Row/Box,为什么还要用 ConstraintLayout?

我的回答是:当布局复杂到需要“相对定位”时,ConstraintLayout 就是救星。比如一个卡片里,头像在左,标题在右上,描述在右下——用 Column/Row 嵌套会很难受。

2.1 基本用法

@Composable
fun ConstraintLayoutDemo() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        val (avatar, title, desc) = createRefs()

        Image(
            painter = painterResource(id = R.drawable.avatar),
            contentDescription = null,
            modifier = Modifier
                .constrainAs(avatar) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                    width = Dimension.value(48.dp)
                    height = Dimension.value(48.dp)
                }
        )

        Text(
            text = "标题",
            modifier = Modifier.constrainAs(title) {
                top.linkTo(avatar.top)
                start.linkTo(avatar.end, margin = 12.dp)
            }
        )

        Text(
            text = "这是一段描述文字",
            modifier = Modifier.constrainAs(desc) {
                top.linkTo(title.bottom, margin = 4.dp)
                start.linkTo(title.start)
            }
        )
    }
}
核心思想:先 createRefs() 创建引用,再用 constrainAs 绑定约束。每个约束可以指定 linkTo、margin、宽度高度策略。

2.2 链条与辅助线

ConstraintLayout 还支持链条(Chain)和辅助线(Guideline)。链条可以让一组控件均匀分布,辅助线可以按百分比定位。

val guideline = createGuidelineFromStart(0.3f) // 从左侧30%位置

Text(
    text = "对齐辅助线",
    modifier = Modifier.constrainAs(createRef()) {
        start.linkTo(guideline)
    }
)
我的习惯:辅助线特别适合做“对称布局”。比如登录页的输入框,左右各留20%边距,用两条 guideline 搞定。

三、Material3 主题系统

Material3 是 Google 最新的设计语言。说白了,它比 Material2 更强调个性化和动态色彩。

3.1 主题结构

// 定义颜色
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color.White,
    secondary = Color(0xFF625B71),
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE)
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    background = Color(0xFF1C1B1F),
    surface = Color(0xFF1C1B1F)
)

// 应用主题
@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

3.2 使用主题中的颜色

@Composable
fun ThemedText() {
    Text(
        text = "Hello Material3",
        color = MaterialTheme.colorScheme.primary,
        style = MaterialTheme.typography.headlineMedium
    )
}
注意:Material3 的 colorScheme 里多了 tertiary、outline 等新角色。我建议你仔细看看官方配色表,尤其是 tertiary 用于强调色,效果很好。

四、深色模式适配

深色模式不是简单的“黑底白字”。我见过很多 App 切到深色模式后,图片边缘发白、阴影消失、对比度刺眼。

4.1 自动适配

使用 Material3 的 darkColorScheme 和 lightColorScheme,系统会自动根据 isSystemInDarkTheme() 切换。

@Composable
fun MyScreen() {
    val isDark = isSystemInDarkTheme()
    val bgColor = if (isDark) Color(0xFF1C1B1F) else Color(0xFFFFFBFE)
    
    Surface(color = bgColor) {
        // 内容
    }
}

4.2 手动覆盖

有时候用户想手动控制。我习惯用 DataStore 保存一个布尔值,然后传给主题。

var isDark by remember { mutableStateOf(false) }

MaterialTheme(
    colorScheme = if (isDark) DarkColorScheme else LightColorScheme
) {
    // ...
}
我曾经踩过的坑:深色模式下,图片资源如果用了硬编码的白色背景,会非常突兀。建议用 Modifier.background(MaterialTheme.colorScheme.surface) 动态设置。

五、知识体系总览

下面这张图,是我梳理的本章核心脉络。你可以把它当作“地图”,随时回来对照。

Compose 布局与主题知识体系 布局容器 Column / Row / Box ConstraintLayout 链条 / 辅助线 Material3 主题 ColorScheme Typography Shape 深色模式 自动适配 手动覆盖 资源适配 核心原则:组合优于继承,声明式描述 UI 布局容器负责结构,主题系统负责视觉,深色模式负责体验

六、总结

这一章我们聊了 Compose 的三大布局容器、ConstraintLayout 的灵活用法、Material3 主题系统,以及深色模式的适配技巧。

我个人觉得,Compose 最爽的地方在于——所有 UI 都是 Kotlin 代码。没有 XML 的割裂感,没有 findViewById 的繁琐。你写布局就像写业务逻辑一样自然。

嗯,下一章我们会深入 Compose 的状态管理与副作用。但那是后话了。先把今天的代码跑起来,感受一下“声明式 UI”的魅力吧。


公众号:蓝海资料掘金营,微信deep3321