鸿蒙原生 ArkTS 布局深度对比:Flex vs Stack,何时用谁?



一、引言:布局是 UI 开发的基石
在鸿蒙原生应用开发中,布局(Layout)决定了组件在屏幕上的排列方式与呈现效果。ArkTS 作为 HarmonyOS NEXT 的声明式 UI 语言,提供了多种布局容器供开发者选择:Flex、Stack、Row、Column、Grid、RelativeContainer......其中最常用、也最容易被混淆的,当属 Flex 与 Stack。
你可能遇到过这样的困扰:
- 想实现一个右下角浮动按钮,用 Flex 硬是排不到正确位置?
- 想做一组重叠头像,写出来的代码嵌套了五六层容器?
- 明明是水平排列的几个标签,却莫名其妙地堆在了一起?
这些问题的根源,往往是对 Flex 和 Stack 本质定位的理解偏差。
Flex 是一维线性布局 ------ 子组件沿着「主轴」依次排列,擅长处理「依次排开」的场景;
Stack 是层叠布局 ------ 子组件在 Z 轴上堆叠,配合定位参数实现覆盖效果,擅长处理「层层覆盖」的场景。
理解了两者的核心区别,布局选型就不再是难题。本文将从布局原理出发,结合完整可运行的 ArkTS 代码示例,帮助你建立一套清晰的选型决策框架。
二、Flex 布局:一维线性排列的艺术
2.1 核心概念
Flex 布局的核心是「主轴」(main axis)和「交叉轴」(cross axis)。通过设置 direction 属性,你可以控制子组件沿水平方向(FlexDirection.Row)或垂直方向(FlexDirection.Column)排列。
typescript
Flex({ direction: FlexDirection.Row }) { /* 水平排列 */ }
Flex({ direction: FlexDirection.Column }) { /* 垂直排列 */ }
在主轴方向上,justifyContent 控制子组件的对齐与间距分布;在交叉轴方向上,alignItems 控制子组件的对齐方式。
关键语法提醒 :在 ArkTS 中,justifyContent 和 alignItems 是 Flex 构造函数的参数,而不是链式调用的属性方法。这是与 CSS Flexbox 最大的语法差异。
typescript
// ✅ 正确写法:在构造函数中声明
Flex({
direction: FlexDirection.Row,
justifyContent: FlexAlign.SpaceEvenly,
alignItems: ItemAlign.Center
}) {
Text("A")
Text("B")
Text("C")
}
// ❌ 错误写法:不能链式调用
Flex({ direction: FlexDirection.Row }) {
// ...
}
.justifyContent(FlexAlign.SpaceEvenly) // 编译报错
2.2 典型场景一:水平工具栏
最经典的 Flex 应用场景就是底部 Tab 栏或顶部导航栏。多个菜单项均匀分布,Flex 的 SpaceEvenly 或 SpaceAround 让间距分配变得轻而易举。
typescript
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceAround }) {
this.TabItem("首页")
this.TabItem("发现")
this.TabItem("消息")
this.TabItem("我的")
}
.width('90%')
.height(60)
.backgroundColor("#FFFFFF")
.borderRadius(12)
2.3 典型场景二:纵向表单或列表
当需要将多个输入框或卡片从上到下排列时,FlexDirection.Column 是最直观的选择。配合 alignItems 控制水平方向的对齐,可以快速搭建出规整的表单界面。
typescript
Flex({
direction: FlexDirection.Column,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceEvenly
}) {
this.ColorBlock("1", "#3F51B5", 50, 40)
this.ColorBlock("2", "#00BCD4", 50, 40)
this.ColorBlock("3", Color.Green, 50, 40)
}
.width('90%')
.height(180)
.backgroundColor("#FFFFFF")
.borderRadius(12)
2.4 典型场景三:流式标签(FlexWrap)
Flex 布局还支持换行能力。通过设置 wrap: FlexWrap.Wrap,当子组件总宽度超过容器时,会自动折行排列,非常适合标签云、关键词列表等场景。
typescript
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap,
justifyContent: FlexAlign.Start
}) {
this.TagItem("ArkTS")
this.TagItem("HarmonyOS NEXT")
this.TagItem("Flex")
this.TagItem("Stack")
// ...更多标签
}
这一特性是 Stack 布局完全无法做到的 ------ Stack 不支持换行,所有子组件只能层叠在一起。
2.5 Flex 的适用边界
Flex 最擅长的是「在一个方向上依次排列多个同级别子组件」。当你有以下需求时,Flex 是首选:
- 导航栏 / Tab 栏 / 工具栏
- 卡片列表 / 纵向表单
- 标签云 / 按钮组
- 任何「线性排列」的 UI 段落
但如果需要子组件之间发生重叠 ,或者需要精确定位到容器的某个角落,Flex 就会暴露出它的局限性 ------ 这时候就该 Stack 登场了。
三、Stack 布局:层叠的魔法
3.1 核心概念
如果说 Flex 是「排队」,那 Stack 就是「叠罗汉」。Stack 中的所有子组件默认从左上角开始,按照代码书写顺序在 Z 轴上依次堆叠 ------ 后写的组件覆盖在先写的组件之上。
typescript
Stack({ alignContent: Alignment.TopEnd }) {
// 底层:卡片
this.ColorBlock("卡片内容", "#607D8B", 200, 120)
// 顶层:徽章(右上角)
this.ColorBlock("5", "#FF5722", 30, 30, true, { top: -8, right: -8, bottom: 0, left: 0 }, 15)
}
alignContent 参数控制所有子组件在 Stack 内的默认对齐方式,可选项包括 TopStart、TopEnd、Center、BottomEnd 等九个方位。
3.2 典型场景一:徽章 / 角标
这是 Stack 最经典的应用:一张卡片或一个头像上,在右上角叠加一个数字徽章。Flex 要做到这种效果需要复杂的嵌套和绝对定位模拟,而 Stack 天然支持。
┌──────────────────┐
│ 卡片内容 │
│ │ ┌──┐
│ │ │5 │
└──────────────────┘ └──┘
Stack({ alignContent: Alignment.TopEnd })
3.3 典型场景二:遮罩层
图片上叠加半透明遮罩,再在遮罩之上显示文字标题 ------ 这在图片轮播、视频封面等场景中极为常见。Stack 的三层结构清晰明了:
- 底层:图片或背景色块
- 中层 :半透明遮罩(如
#00000060) - 顶层:标题文字
typescript
Stack({ alignContent: Alignment.Center }) {
// 底层:背景
this.ColorBlock("", "#3F51B5", 280, 140, false, undefined, 16)
// 中层:半透明遮罩
this.ColorBlock("", "#00000060", 280, 140, false, undefined, 16)
// 顶层:文字
Text("HarmonyOS NEXT")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
3.4 典型场景三:浮动操作按钮(FAB)
Material Design 风格的浮动按钮固定在右下角,页面内容滚动时按钮始终可见。Stack 配合 Alignment.BottomEnd 可以轻松实现。
typescript
Stack({ alignContent: Alignment.BottomEnd }) {
// 底层:页面内容
this.ColorBlock("页面内容区", "#E0E0E0", 280, 140, false, undefined, 12)
// 顶层:浮动 "+" 按钮
Flex({ ... }) {
Text("+").fontSize(28).fontColor(Color.White)
}
.width(50).height(50)
.backgroundColor("#FF4081")
.borderRadius(25)
.margin({ bottom: 16, right: 16 })
}
3.5 典型场景四:重叠头像组
社交应用中常看到的多人头像重叠效果(类似 Slack 或钉钉的群头像),是 Stack 展示其定位能力的绝佳案例。每个头像通过 offset 向右偏移固定像素,形成层叠效果。
typescript
Stack({ alignContent: Alignment.Start }) {
this.AvatarItem(Color.Red, "A", 0) // 左:0
this.AvatarItem(Color.Blue, "B", 22) // 偏移 22px
this.AvatarItem(Color.Green, "C", 44) // 偏移 44px
this.AvatarItem("#9C27B0", "D", 66) // 偏移 66px
Text("+2").offset({ x: 88, y: 0 }) // 更多提示
}
3.6 Stack 的适用边界
Stack 最适合以下场景:
- 徽章 / 角标 / 未读计数
- 图片遮罩 + 文字叠加
- 浮动操作按钮(FAB)
- 重叠元素(头像、卡片)
- 全屏加载遮罩 / 弹窗背景
Stack 的局限性在于:
- 不支持换行:所有子组件都在同一个平面层上
- 子组件默认重叠:如果不小心把所有组件放到 Stack 里,它们会堆在一起而不是依次排列
- 不适合列表类场景:线性排列需要手动计算位置,远不如 Flex 高效
四、选择指南:一个决策框架
三思而后行,布局选型也是一样。我总结了一套「两步判断法」,帮助你在 5 秒钟内做出正确选择。
4.1 第一步:是否需要重叠?
问自己一个问题:子组件之间要不要在视觉上发生重叠?
- ✅ 是 → 用 Stack
- ❌ 否 → 继续第二步
重叠是 Stack 独有的能力。需要徽章覆盖在头像上?用 Stack。需要文字浮在图片上?用 Stack。需要浮动按钮定在角落?用 Stack。
4.2 第二步:是否需要沿一个方向依次排列?
如果不需要重叠,再问第二个问题:子组件是否需要沿着一条直线(水平或垂直)依次排列?
- ✅ 是 → 用 Flex
- ❌ 否 → 考虑其他布局(Grid、RelativeContainer 或自定义布局)
Flex 就是为「线性排列」而生的。工具栏、列表、标签栏、表单 ------ 只要是依次排开的场景,Flex 都是最简洁高效的解法。
4.3 决策流程图
开始
│
▼
子组件需要重叠/层叠吗?
│ │
├── 是 ──► 用 Stack
│
▼ 否
子组件需要沿一个方向依次排列吗?
│ │
├── 是 ──► 用 Flex
│
▼ 否
考虑 Grid / RelativeContainer / 自定义布局
4.4 完整对比表
| 对比维度 | Flex | Stack |
|---|---|---|
| 排列方向 | 一维(行或列) | Z 轴层叠 |
| 定位方式 | 自动排列(flex-flow) | 绝对/相对定位(alignContent + offset) |
| 重叠支持 | ❌ 不支持 | ✅ 天然支持 |
| 换行能力 | ✅ FlexWrap.Wrap | ❌ 不支持 |
| 内容适配 | ✅ 弹性伸缩(flex-grow) | ❌ 固定尺寸需手动计算 |
| 典型场景 | 工具栏、列表、标签、表单 | 徽章、遮罩、FAB、重叠头像 |
五、代码实现中的关键语法注意事项
在编写上述示例代码的过程中,我遇到了几个 ArkTS 特有的语法限制,值得单独说明。
5.1 justifyContent 和 alignItems 必须写在构造函数中
这是 ArkTS 与 CSS Flexbox 最大的不同。CSS 中你可以写:
css
.flex-container {
display: flex;
justify-content: center;
align-items: center;
}
但 ArkTS 中,这些属性必须作为 Flex 构造函数的参数传入:
typescript
// ✅ 正确
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.Center,
alignItems: ItemAlign.Center
}) { /* ... */ }
// ❌ 编译错误
Flex({ direction: FlexDirection.Column }) { /* ... */ }
.justifyContent(FlexAlign.Center)
5.2 @Builder 函数返回 void,外部不可链式调用
在 ArkTS 中,@Builder 修饰的函数用于封装 UI 片段,但它返回 void 。这意味着你不能在调用 @Builder 的地方继续链式调用属性方法:
typescript
// ❌ 编译错误:ColorBlock 是 @Builder,返回 void
this.ColorBlock("5", "#FF5722", 30, 30, true)
.margin({ top: -8, right: -8 })
.borderRadius(15)
正确的做法是将需要定制的属性作为参数传入 Builder 内部:
typescript
@Builder
ColorBlock(label: string, bgColor: ResourceColor, w: number, h: number,
isCircle: boolean = false,
extraMargin?: Margin,
extraBorderRadius?: number) {
Flex({ ... }) {
Text(label).fontColor(Color.White)
}
.width(w).height(h)
.backgroundColor(bgColor)
.borderRadius(extraBorderRadius ?? (isCircle ? (w / 2) : 8))
.margin(extraMargin ?? 0)
}
// ✅ 正确:参数在 Builder 内部使用
this.ColorBlock("5", "#FF5722", 30, 30, true,
{ top: -8, right: -8, bottom: 0, left: 0 }, 15)
5.3 Color 枚举的局限性
ArkTS 的 Color 枚举只包含有限的预设颜色:Red、Green、Blue、Orange、Yellow、White、Black、Gray 等。没有 Cyan、Purple、Pink 等。如果需要这些颜色,必须使用十六进制字符串:
typescript
// ❌ 编译错误:Color.Cyan 不存在
const COLOR_FLEX_SECONDARY: Color = Color.Cyan;
// ✅ 正确:使用字符串色值
const COLOR_FLEX_SECONDARY: ResourceColor = "#00BCD4";
5.4 Router 导航的使用
在 HarmonyOS NEXT API 24 中,页面间导航推荐使用 router.pushUrl。需要注意的是,pushUrl 的无参数重载已被标记为弃用,应显式传入 RouterMode:
typescript
import { router } from '@kit.ArkUI';
router.pushUrl(
{ url: 'pages/FlexVsStack' },
router.RouterMode.Standard
);
同时不要忘记在 main_pages.json 中注册目标页面:
json
{
"src": [
"pages/Index",
"pages/FlexVsStack"
]
}
六、结语
布局选型没有银弹,但掌握 Flex 和 Stack 各自的「性格」之后,大多数场景都能在几秒内找到最优解。
Flex 是理性的线性思维 ------ 它把一切排成一条线,有序、规整、可预测。
Stack 是感性的层叠思维 ------ 它允许元素跨越层级,创造深度和重叠的视觉效果。
在实际项目中,Flex 和 Stack 不是互斥的,而是互为补充的。你完全可以在一个 Stack 内部嵌套 Flex(比如浮动按钮内部的文字用 Flex 居中),也可以在 Flex 的某个子项中用 Stack 叠加内容。真正优秀的 UI 代码,是两种布局的灵活组合。
最后,记住这个口诀:
「若要排排坐,Flex 准没错;若要叠叠高,Stack 领风骚。」
希望这篇文章对你的鸿蒙应用开发有所帮助。如果你在实际项目中有更多布局相关的疑问或心得,欢迎交流探讨。