【共创季稿事节】鸿蒙原生 ArkTS 布局深度对比:Flex vs Stack

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


一、引言:布局是 UI 开发的基石

在鸿蒙原生应用开发中,布局(Layout)决定了组件在屏幕上的排列方式与呈现效果。ArkTS 作为 HarmonyOS NEXT 的声明式 UI 语言,提供了多种布局容器供开发者选择:Flex、Stack、Row、Column、Grid、RelativeContainer......其中最常用、也最容易被混淆的,当属 FlexStack

你可能遇到过这样的困扰:

  • 想实现一个右下角浮动按钮,用 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 中,justifyContentalignItems 是 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 的 SpaceEvenlySpaceAround 让间距分配变得轻而易举。

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 内的默认对齐方式,可选项包括 TopStartTopEndCenterBottomEnd 等九个方位。

3.2 典型场景一:徽章 / 角标

这是 Stack 最经典的应用:一张卡片或一个头像上,在右上角叠加一个数字徽章。Flex 要做到这种效果需要复杂的嵌套和绝对定位模拟,而 Stack 天然支持。

复制代码
┌──────────────────┐
│  卡片内容          │
│                   │   ┌──┐
│                   │   │5 │
└──────────────────┘   └──┘
         Stack({ alignContent: Alignment.TopEnd })

3.3 典型场景二:遮罩层

图片上叠加半透明遮罩,再在遮罩之上显示文字标题 ------ 这在图片轮播、视频封面等场景中极为常见。Stack 的三层结构清晰明了:

  1. 底层:图片或背景色块
  2. 中层 :半透明遮罩(如 #00000060
  3. 顶层:标题文字
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 枚举只包含有限的预设颜色:RedGreenBlueOrangeYellowWhiteBlackGray 等。没有 CyanPurplePink。如果需要这些颜色,必须使用十六进制字符串:

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 领风骚。」

希望这篇文章对你的鸿蒙应用开发有所帮助。如果你在实际项目中有更多布局相关的疑问或心得,欢迎交流探讨。