Jetpack Compose 一直在快速演进。随着新的 FlexBox 布局到来,我们终于有了一种强大且灵活的方式来构建自适应 UI。
不言而喻,它的设计理念正是源自 Web 领域的 CSS Flexbox 模型,并且在概念、术语和行为方面几乎完全相同。如果您熟悉 display: flex,就会发现 FlexBox 的属性和行为几乎完全相同。
在 FlexBox 之前,如果你想做“横向/纵向的线性排列”,通常会用 Row、Column;如果又需要根据空间自动换行,则会转向 FlowRow / FlowColumn。而 FlexBox 更像是它们能力的“超集”:在保留熟悉使用体验的同时,提供了对对齐、换行与子项分配方式更细的控制。
过去的我们常在“刚性布局”(Row/Column)与“更动态的布局”(FlowRow/FlowColumn)间二选一,但是现在,FlexBox 试图把两者的优势合并在一起——既灵活,又尽量保持简单。
对于用过 Row、Column 或 FlowRow 的人,或者你有过 Web CSS3 的开发经验,上手 FlexBox 会很顺畅。
何时用
FlexBox 通常用于在整个屏幕布局中显示少量项,且希望根据不同屏幕尺寸自动调整的布局。
但是 FlexBox 不支持延迟加载。如需显示大量项,请使用 LazyRow 或者 LazyColume 等这种延迟布局,这样会得到更好的效率。
换句话说,如果你的布局又多又复杂,那么把 FlexBox 放到 LazyColume 中,当做 LazyColume 的子项会是不错的选择。
第一个布局
在开始使用之前,请使用最新的 Compose Bom 依赖,当然,这样做实际上还不能使用 FlexBox,我们还需要将 foundation-layout 更新到最新版本
implementation("androidx.compose.foundation:foundation-layout:1.11.0-beta02")
我当然推荐你使用 lib.versions.toml 去管理依赖,这里只是提醒你别忘了更新 foundation-layout。
好的,现在我们就能使用 FlexBox 了,先来第一个简单布局:
FlexBox(
config = {
wrap(FlexWrap.Wrap)
gap(8.dp)
}
) {
// 每个 Box 都有一个 100.dp 的初始大小
// 有些元素会自动扩展,填满该行剩余的所有空间
RedRoundedBox()
BlueRoundedBox()
GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) })
OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) })
PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) })
}
哦,对了,此时如果你写这段代码,你会得到一个实验性 API 的提示。为了方便,此时我们在顶层文件中声明这个 @file:OptIn(ExperimentalFlexBoxApi::class),这样整个文件都不会有这个提示了。
另外说明一下,上述代码以及后续代码中出现的 XXXRoundedBox 都有固定的 100dp 大小的尺寸,它的默认代码如下:
@Composable
fun RedRoundedBox(modifier: Modifier = Modifier) {
Spacer(
modifier = modifier
.size(100.dp)
.clip(RoundedCornerShape(16.dp))
.drawBackground("#fb2c36".color)
)
}
其他的 XXXRoundedBox 只是颜色上有区分,其它的设置都是一样的。
上述代码 FlexBox 会将五个子项换行到两行,并以不同的比例展开它们,以填充每行中的可用空间。各项之间存在 8.dp 的垂直和水平间距。
我知道你一定会有很多疑问,不急,我们往下看。
容器行为
FlexBox 的容器行为需要通过单独的 config 去配置:
FlexBox(
config = {
direction(FlexDirection.Column)
wrap(FlexWrap.Wrap)
alignItems(FlexAlignItems.Center)
alignContent(FlexAlignContent.SpaceAround)
justifyContent(FlexJustifyContent.Center)
gap(16.dp)
}
) { // child items
}
这是一个稍微复杂点的示例,下面我们详细介绍一下每个配置项的作用。
gap
这里我先介绍 gap,其实 gap 分 rowGap 和 columnGap 在行和列之间添加间距。而 gap 是一个便捷函数,可同时添加 columnGap 和 rowGap。
FlexBox(
config = {
wrap(FlexWrap.Wrap)
rowGap(12.dp)
}
) {
RedRoundedBox()
BlueRoundedBox()
GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) })
OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) })
PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) })
}
注意,rowGap 设置的是行之间的,也就是竖向的边距。
direction
direction 函数用于设置布局的方向,也就是决定子项的布局方向。它接受以下值:
Row(默认):水平方向布局。在从左到右的语言区域中,此值为从左到右;在从右到左的语言区域中,此值为从右到左。RowReverse:反转Row的方向。Column:垂直方向(从上到下)布局。ColumnReverse:反转Column的方向。
FlexBox(
config = {
wrap(FlexWrap.Wrap)
direction(FlexDirection.Column)
gap(8.dp)
},
modifier = Modifier.height(300.dp)
) {
RedRoundedBox()
BlueRoundedBox()
GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) })
OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) })
PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) })
}
如果这里不给 height(300.dp),那么 FlexBox 就会像普通的 Column 一样向下布局。
justifyContent
justifyContent 用于确定在布局方向上,如何分配子项。下表显示了当方向为 Row 时的行为:
如果你熟悉 LazyColumn 或者 LazyRow 的话,相信你理解这些属性是信手拈来。
alignItems
alignItems 用于确定在单行中沿布局方向是如何对子项对齐的。当然,各个子项也可以使用 alignSelf 来替换此行为。
以下图片展示了方向为 Row 时的行为:
alignContent
alignContent 可将各行对齐到布局方向上,并在各行之间分配额外的空间。此属性仅在有多行文本(启用换行)时适用。以下图片展示了当方向为 Row 时的行为:
alignItems和alignContent的区别:alignItems用于设置单行的对齐方式;而alignContent是设置整个子项的对齐方式,它将这个子项看作一个整体。
wrap
wrap 用于设置换行行文,该功能可让 FlexBox 容器变为多行,并将无法放下的子项沿布局方向移动到新行或新列。
wrap 需要一个 FlexWrap 类型,FlexWrap 支持三个值:
NoWrap(默认值):不换行。如果主尺寸不足,则会溢出。Wrap:当没有足够的空间来放置某个子项时,系统会按照布局方向上创建新行。例如,如果方向为Row,则会在下方添加新行。WrapReverse:与Wrap相同,只不过新行是沿与布局方向相反的方向添加的。例如,如果方向为Row,则会在上方添加新行。
我们对比下 NoWrap 和 Wrap:
FlexBox(
config = {
wrap(FlexWrap.NoWrap)
direction(FlexDirection.Row)
gap(8.dp)
},
) {
RedRoundedBox()
BlueRoundedBox()
GreenRoundedBox()
OrangeRoundedBox()
PinkRoundedBox()
}
如果改成 Wrap,则会自动换行,会保证所有内容显示下:
厉害吧!换行——我个人认为就是响应式布局的核心。
简述运作原理
我们用一个简单例子,来简述 FlexBox 的运作方式。假设 FlexBox 容器的主要大小为 100dp,wrap 设置为 FlexWrap.Wrap —— 即可以换行,间距为 8dp。它包含三个项目,分别具有 20dp、40dp 和 50dp。
该行有 100dp 可用空间。子项目 1 为 20dp。 有空间,因此将 1 放置到该行中:
此时,该行还有 80dp 可用空间。间距为 8dp。子项目 2 为 40dp。所需空间为 48dp。有空间,因此 2 也会放置到该行中。
现在,该行有 32dp 个可用空间。间距为 8dp。子项目 3 为 50dp。所需空间为 58dp。
糟糕,当前行中的空间不足,因此 3 放置在新行中。
子项行为
当我们研究完容器的属性和行为之后,我们来看看作为 FlexBox 的子项,有哪些行为可以设置。
子项的行为设置比较特殊,需要使用 Modifier.flex,它可以控制 FlexBox 内的子项如何更改大小、顺序和对齐方式。
使用 basis、grow 和 shrink 函数来控制子项的大小。例如:
FlexBox {
RedRoundedBox(
modifier = Modifier.flex {
basis(FlexBasis.Auto)
grow(1.0f)
shrink(0.5f)
}
)
}
basis
basis 可指定在分配任何额外空间之前商品的初始大小。可以将此值视为商品的首选尺寸。
首选尺寸的意思是:先用这个尺寸试试看!下面会细讲
basis 有三种设置方式:
- Auto:使用子项的最大固有尺寸。
- 固定 dp:以
Dp为单位的固定大小。 - 百分比:容器大小的百分比。
下面依次举例说明。
FlexBox(
config = {
wrap(FlexWrap.Wrap)
direction(FlexDirection.Row)
},
) {
RedRoundedBox(modifier = Modifier.flex { basis(FlexBasis.Auto) })
BlueRoundedBox(modifier = Modifier.flex { basis(FlexBasis.Auto) })
GreenRoundedBox()
BlueRoundedBox()
}
FlexBasis.Auto 会直接使用子项的本身设定的尺寸。
如果将第一个 RedRoundedBox 改成 RedRoundedBox(modifier = Modifier.flex { basis(300.dp) }),你会发现如下的效果:
当使用 basis(300.dp) 需要注意两个点:
- 只影响布局方向的尺寸,例如当前布局是
Row(默认) 布局,即只会影响横向的尺寸。 300.dp大于子项设定的100.dp,所以这里使用了300.dp作为子项的布局后的尺寸。
如果使用 basis(10.dp) 呢?这个尺寸可比 100.dp 要小:
这里就体现了 basis 的特点了,如果 basis 小于商品的固有最小尺寸,则改用固有最小尺寸。例如,如果使用 Text 去包含某个字词需要 50dp 才能显示,但同时具有 basis(10.dp),则使用 50dp 的值。
如果使用百分比呢?
FlexBox(
config = {
wrap(FlexWrap.Wrap)
direction(FlexDirection.Row)
},
) {
RedRoundedBox(modifier = Modifier.flex { basis(0.4f) })
BlueRoundedBox(modifier = Modifier.flex { basis(0.6f) })
}
basis(0.4f) 表示使用父布局 FlexBox 空间的 0.4 倍(可不是剩余空间!而是父布局的所有空间)。
grow
grow 表示,如果还有额外空间时,子项的增长量。
这是 FlexBox 容器中在所有商品的 basis 值相加后剩余的空间。
grow 值表示与同级元素相比,指定子元素将获得多少额外空间(这个额外空间很重要)。默认情况下,子项不会自动增长填充剩余空间。
我们来看看如果只设置一个 grow 的情况。
FlexBox(
config = {
wrap(FlexWrap.Wrap)
direction(FlexDirection.Row)
},
) {
RedRoundedBox(modifier = Modifier.flex { grow(1f) })
BlueRoundedBox()
GreenRoundedBox()
}
因为只是设置了红色的子项,所以红色子项会填充剩余的所有空间。
如果都给呢?
FlexBox(
config = {
wrap(FlexWrap.Wrap)
direction(FlexDirection.Row)
},
) {
RedRoundedBox(modifier = Modifier.flex { grow(1f) })
BlueRoundedBox(modifier = Modifier.flex { grow(2f) })
GreenRoundedBox(modifier = Modifier.flex { grow(3f) })
}
红蓝绿三个子项,会按照 1:2:3 的比例,去分配剩余空间!
shrink - 暂时不可用
既然有 grow,那么相应的,也会有 shrink。
当 FlexBox 容器没有足够的空间容纳所有子项时,使用 shrink 设置子项的缩放程度。
shrink 的工作方式与 grow 相同,只不过它不是将额外空间分配给项,而是将空间不足分配给项。
shrink 值用于指定商品获得多少空间不足量,或者更确切地说,商品将缩小多少。
默认情况下,商品的 shrink 值为 1f,也就是说,所有的子项都会等比例缩小。如果 shrink 为 0f,表示该子项坚决不缩小。
但是!为什么这里说不可用呢?
截止到 2026/04/02,关于 shrink,都是 Compose 期望做成这样。
我测试这个参数,还是不可用的状态,无论你怎么调整,这个参数都不起作用。如果想学习更多关于这个参数的信息,可以看这里。
alignSelf
使用 alignSelf 控制子项的对齐方式,此属性会覆盖容器的 alignItems。他们具有所有相同的设定值,并添加了 Auto,后者继承了 FlexBox 容器的行为。
FlexBox(
config = {
alignItems(FlexAlignItems.Start)
},
modifier = Modifier.height(300.dp)
) {
RedRoundedBox()
BlueRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Center) })
GreenRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.End) })
}
虽然我们给容器设置了 FlexAlignItems.Start,但是可以通过 alignSelf 给子项重新设置对齐方式。
order
默认情况下,FlexBox 会按代码中声明的顺序布局项。
你可以通过给子项设置 order 覆盖此行为。
order 的默认值为零,FlexBox 会根据此值以升序对商品进行排序。具有相同 order 值的任何项都将按照声明时的顺序进行布局。使用负值和正值 order 可将项移至布局的开头或结尾,而无需更改其声明位置。
以下示例展示了两个子项。第一个的默认 order 为零,第二个的顺序为 -1。排序后,红色显示在蓝色之后:
FlexBox {
RedRoundedBox()
BlueRoundedBox(
modifier = Modifier.flex {
order(-1)
}
)
}
一点想法
回过头来看,FlexBox 并不是什么全新的发明——它本质上就是把 Web 前端早已玩透的 CSS3 Flexbox 搬进了 Compose。但这恰恰是它最大的优势:经过无数前端项目验证的布局模型,天然就比从零设计一套新方案更靠谱。
对于我们 Android 开发者来说,以前用 Row、Column 能搞定大部分场景,遇到自动换行就换成 FlowRow / FlowColumn,但总有些"既要又要"的时刻让人左右为难。
FlexBox 把这些能力攒到了一起,direction、wrap、grow、basis 这些属性组合起来,能覆盖的场景确实比以前宽了不少。
当然,目前它还不够完善——shrink 还是摆设,延迟加载也没有支持,API 还在 beta 阶段随时可能调整。所以现阶段拿它做一些个人项目、演示项目是没问题的,但大规模用到生产环境还需要再观望观望。
如果你之前写过 CSS3 Flexbox,上手这套东西基本没什么门槛;如果没写过,趁这个机会把 Flexbox 的概念学一遍也不亏——毕竟这套思路在前端和移动端都通用,学一次,两头受益。
Compose 有个宏大的愿景,就是尽可能统一不同端的界面开发体验。可一旦真要往多端走,响应式这件事就绕不过去:同一套描述 UI 的方式,不能只在手机这一种尺寸上工作得漂亮,还得能比较自然地适配平板、桌面,甚至未来更多形态的设备。
从这个角度看,FlexBox 应该只是一个开始,它背后反映出来的,其实是 Compose 正在认真补齐自己在响应式布局上的能力。
不过这里我也有另一点担忧,那就是 Modifier 可能已经快扛不住现在的需求了。它最开始的设计确实很优雅,一个链式调用把布局、测量、绘制、交互这些能力全都串起来,用起来也很统一;但随着 Compose 能做的事情越来越多,Modifier 身上背的职责也越来越重。现在很多时候你写一个 Modifier 链,实际上已经不是单纯在“修饰”一个组件了,而是在同时参与多个阶段的行为定义。
Modifier 会不会被拆成更明确的几类能力,比如分别处理布局、绘制、交互,甚至出现更细的作用域约束,我觉得这一天大概率会来。
对我来说,FlexBox 有意思的地方,不只是“Compose 多了一个新布局”,而是它让人隐约看见了 Compose 下一阶段可能会怎么演进。