Compose 终于上线 FlexBox:换行与弹性伸缩 都轻松搞定!

前言

用 Compose 写布局的人,多多少少都经历过这种纠结:

  • Row 和 Column,够简单,但不会换行------子项一旦超出容器宽度,直接溢出。
  • 从foundation-layout 1.4 起,FlowRow 和 FlowColumn(),终于能换行了,但不支持弹性伸缩------你没办法让某个子项按比例占满剩余空间
  • 想要「换行 + 弹性伸缩」?以前只有两条路:要么套 AndroidView 调 Google 的老库 flexbox-layout(一个 View 体系的 RecyclerView LayoutManager),要么自己写 Layout 手算。

2026 年 4 月,Compose Foundation 1.9.0-alpha01 正式引入了 FlexBox 布局:

  • 一个原生的、声明式的、完整对齐 CSS Flexbox 语义的容器
  • 换行、弹性伸缩、对齐控制、间距分配,一个 Composable 全搞定。

今天,从「为什么它必须存在」的设计动机出发,把 FlexBox 的能力边界、与 FlowRow 的本质区别、核心 API 的用法、布局算法的内部流程,一次讲清楚。


Compose FlexBox 是什么

  • Compose Foundation 新增的弹性盒子布局容器,完整对齐 CSS Flexbox 规范语义;
  • 支持子项在主轴方向上自动换行 (wrap)和弹性伸缩(flex-grow / flex-shrink)。

与 CSS Flexbox 的关系

如果你写过 Web 前端,对 display: flex 一定不陌生。

Compose FlexBox 的设计目标就是把 CSS Flexbox 的核心能力搬到 Compose 声明式体系中,同时做了 Compose 化的适配:

CSS Flexbox Compose FlexBox
display: flex FlexBox { }
flex-direction FlexDirection.Row / Column
flex-wrap FlexWrap.Wrap / NoWrap
justify-content JustifyContent.SpaceBetween 等
align-items AlignItems.Center 等
flex-grow / flex-shrink Modifier.flex { grow(1f); shrink(0f) }
align-self Modifier.flex { alignSelf(AlignSelf.Stretch) }
order Modifier.flex { order(2) }

关键不同 :CSS Flexbox 默认不换行(flex-wrap: nowrap),而 Compose FlexBox 默认就是 Wrap 模式------这是因为移动端屏幕尺寸多变,换行是更安全的默认行为。


FlexBox特性预览

完整对齐 CSS Flexbox 语义,为 Compose 带来 换行 + 弹性伸缩 的原生能力

FlexBox vs Compose已有方案

三代布局的本质差异如下:

能力维度 Row / Column FlowRow / FlowColumn FlexBox
换行 ❌ 不支持 ✅ 自动换行 ✅ 自动换行
弹性伸缩 ❌ 只有 weight ❌ 不支持 ✅ flex { grow(); shrink() }
主轴对齐 Arrangement Arrangement JustifyContent(更丰富)
交叉轴对齐 Alignment Alignment AlignItems + AlignSelf
子项排序 ❌ ❌ ✅ order()
行间距控制 N/A maxItemsInEachRow 自动按 wrap 分行 + gap
shrink 溢出收缩 ❌ ❌ ✅ 按比例收缩
适用场景 简单单行/单列 标签流、Chip 组 自适应卡片、仪表盘、复杂响应式

关键差异:weight ≠ flex-grow

很多人以为 Row 里的 Modifier.weight(1f) 就是 flex-grow------不是的 。weight 只在固定数量的子项之间分配固定空间 ,它不参与换行计算。而 flex-grow 是在换行后的每一行内,对剩余空间做二次分配。

举个例子:5 个子项,每个 min-width 200dp,容器宽 1000dp。

  • Row + weight:5 个全挤在一行,每个刚好 200dp(或等分),不会换行
  • FlowRow:3 个一行、2 个一行,但第一行右侧有空白无法填满
  • FlexBox + grow(1f):3 个一行、2 个一行,每行的剩余空间按 grow 比例自动填满

这就是 FlexBox 存在的核心理由:换行之后,每一行内部还能做弹性伸缩。


核心 API 详解

完整的 FlexBox 配置体系 = 容器级 6 参数 + 子项级 5 属性

容器级配置

FlexBox 的容器配置通过参数直接传入:

kotlin 复制代码
@Composable
fun FlexBox(
    modifier: Modifier = Modifier,
    flexDirection: FlexDirection = FlexDirection.Row,
    flexWrap: FlexWrap = FlexWrap.Wrap,
    justifyContent: JustifyContent = JustifyContent.FlexStart,
    alignItems: AlignItems = AlignItems.Stretch,
    alignContent: AlignContent = AlignContent.FlexStart,
    gap: Dp = 0.dp,
    content: @Composable FlexBoxScope.() -> Unit
)

各参数说明:

  • flexDirection:主轴方向,Row(水平,默认)或 Column(垂直)
  • flexWrap:是否换行,Wrap(默认)或 NoWrap
  • justifyContent:主轴方向的对齐与分布,支持 FlexStart / FlexEnd / Center / SpaceBetween / SpaceAround / SpaceEvenly
  • alignItems:交叉轴方向子项的默认对齐,支持 FlexStart / FlexEnd / Center / Stretch / Baseline
  • alignContent:多行场景下行与行之间的对齐方式
  • gap:子项之间的间距(主轴和交叉轴统一)

子项级配置

子项通过 Modifier.flex { } DSL 设置弹性属性:

kotlin 复制代码
FlexBox(
    justifyContent = JustifyContent.SpaceBetween,
    alignItems = AlignItems.Center,
    gap = 12.dp
) {
    // 固定宽度子项
    Box(
        Modifier
            .flex { order(1); shrink(0f) }
            .width(80.dp)
            .height(80.dp)
            .background(Color.Blue)
    )

    // 弹性填充子项
    Text(
        "这段文字会占满剩余空间",
        Modifier.flex { grow(1f) }
    )

    // 自定义交叉轴对齐
    Icon(
        Icons.Default.Star,
        contentDescription = null,
        Modifier.flex { alignSelf(AlignSelf.FlexEnd) }
    )
}

flex { } DSL 速查

函数 默认值 说明
grow(float) 0f 剩余空间分配权重,0 表示不伸展
shrink(float) 1f 空间不足时的收缩权重,0 表示不收缩
basis(Dp) Dp.Unspecified 参与 flex 计算前的初始尺寸
order(int) 0 视觉排序优先级,值越小越靠前
alignSelf(AlignSelf) Auto 覆盖容器的 alignItems 设置

最小可运行示例:自适应标签墙

kotlin 复制代码
@Composable
fun AdaptiveTagWall(tags: List<String>) {
    FlexBox(
        flexWrap = FlexWrap.Wrap,
        gap = 8.dp,
        justifyContent = JustifyContent.FlexStart,
        alignItems = AlignItems.Center
    ) {
        tags.forEach { tag ->
            Surface(
                modifier = Modifier.flex { grow(0f); shrink(1f) },
                shape = RoundedCornerShape(20.dp),
                color = MaterialTheme.colorScheme.secondaryContainer
            ) {
                Text(
                    text = tag,
                    modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp),
                    style = MaterialTheme.typography.labelMedium
                )
            }
        }
    }
}

这个标签墙里,每个标签自适应内容宽度,超出容器宽度自动换行。shrink(1f) 保证极端情况下标签可以压缩文字而不溢出。

添加依赖

kotlin 复制代码
// build.gradle.kts
dependencies {
    implementation("androidx.compose.foundation:foundation-layout:1.9.0-alpha01")
}

注意:FlexBox 目前处于 alpha 阶段,API 可能在后续版本中调整。生产环境使用请评估风险。


布局算法

理解内部算法,才能在复杂场景下预测布局行为。Compose FlexBox 的测量流程可以拆成 5 步:

Step 1:计算 Hypothetical Main Size

对每个子项,先用 basis(如果有)或子项自身的内在尺寸,作为假设主轴尺寸。这一步不考虑 grow/shrink,只是确定一个「起点尺寸」。

Step 2:分行(Line Breaking)

按主轴方向依次放置子项。当累计尺寸超过容器主轴长度时,break 到下一行 。这一步的行为取决于 flexWrap:

  • Wrap:超出就换行
  • NoWrap:全部挤在一行(可能溢出)

Step 3:Flex Resolve(弹性解析)

这是 FlexBox 的核心步骤。对每一行独立执行:

  • 如果该行有剩余空间 :按 grow 比例分配给标记了 grow > 0 的子项
  • 如果该行空间不足 :按 shrink 比例从标记了 shrink > 0 的子项中回收

这就是 FlowRow 做不到的事------FlowRow 只做分行,不做行内的弹性解析。

Step 4:主轴对齐(Justify)

弹性解析之后,如果行内还有剩余空间(比如所有子项 grow = 0),就按 justifyContent 的策略分配间距:

  • SpaceBetween:两端顶格,中间等分
  • SpaceAround:每个子项两侧等距
  • SpaceEvenly:所有间隙(包括两端)完全等分

Step 5:交叉轴对齐(Align)

最后一步,确定每个子项在交叉轴上的位置:

  • 先看子项有没有 alignSelf,有就用子项自己的
  • 没有就用容器的 alignItems 默认值
  • Stretch 会把子项撑满行高

这五步跑完,每个子项的 (x, y, width, height) 就全部确定了。


适用场景

什么时候该用 FlexBox,什么时候不该用,怎么用才不踩坑?

✅ 适合用 FlexBox 的场景

1. 自适应卡片网格

仪表盘、数据面板里的卡片需要「宽度自适应、超出换行、行内等分」。FlexBox + grow(1f) + basis(minWidth) 完美解决。

2. 响应式工具栏 / 导航栏

在不同屏幕尺寸下,工具栏按钮需要「能塞就塞一行,塞不下就换行,但每行按钮要等分」。

3. 混合尺寸的标签 / Chip 流

比如搜索页的热门标签,每个标签宽度不同,需要换行 + 视觉均匀分布。

4. 表单布局

输入框 + 标签的组合,在大屏上一行两列,小屏上一行一列,行内的输入框 grow(1f) 占满剩余空间。

❌ 不适合用 FlexBox 的场景

1. 简单的单行水平/垂直排列

Row / Column 就够了,不需要引入 FlexBox 的额外概念成本。

2. 大量列表数据

FlexBox 不支持懒加载 。如果你有几百上千个子项,请用 LazyVerticalGrid 或 LazyVerticalStaggeredGrid。FlexBox 会一次性测量所有子项。

3. 需要固定行列数的网格

如果你需要「固定 3 列」,LazyVerticalGrid(columns = Fixed(3)) 比 FlexBox 更直观。

最佳实践清单

  • 1:总是设 gap,别用 padding 模拟间距------gap 参与 flex 计算,padding 不参与
  • 2:给弹性子项设 basis,别只依赖内在尺寸------basis 是 flex 算法的「起点」,设了才能让 grow/shrink 的计算可预测
  • 3:shrink(0f) 保护关键子项------图标、头像这种不应该被压缩的元素,明确标记 shrink(0f)
  • 4:用 order() 做响应式重排 ------比在不同屏幕尺寸下用 if-else 切换子项顺序优雅得多
  • 5:注意性能边界 ------子项数量控制在 50 个以内,超过这个量级考虑 LazyLayout 方案
  • 6:alpha 阶段做好 API 变更预案------用 wrapper Composable 封装一层,未来 API 变动时只改一处

最后

Compose 的布局能力一直在「够用」和「完备」之间挣扎。FlexBox 是「完备」的最后一块拼图。

它补上的不仅是 flex-grow 和 flex-shrink 这两个 API,而是一整套「容器级约束 + 子项级弹性 + 自动分行」的布局语义。这套语义在 CSS 世界已经被验证了十年,现在终于原生落地到了 Compose。


📎 参考资料

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui