鸿蒙原生 ArkTS 布局方式之 Flex 弹性布局入门:Column 与 Row 的统一抽象

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前言:为什么你需要理解 Flex?

如果你是第一次接触 HarmonyOS 原生应用开发,那么你一定会频繁遇到两个基础布局容器——ColumnRow。前者将子组件沿垂直方向(主轴从上到下)排列,后者将子组件沿水平方向(主轴从左到右)排列。在绝大多数页面场景中,这两个组件确实够用了。但当你开始遇到更复杂的布局需求——比如动态切换排列方向、主轴与交叉轴的精细对齐、子项换行与缩放——你会发现 Column 和 Row 的能力边界很快就被触及了。

这时候就需要引入它们的底层实现者:Flex

Flex 弹性布局是 HarmonyOS ArkUI 框架中最核心、最灵活的布局方案之一。它借鉴了 CSS Flexbox 的设计思想,并针对鸿蒙原生场景做了深度适配。更重要的是,Column 和 Row 本身就是 Flex 的特例

  • Column 等价于 Flex({ direction: FlexDirection.Column })
  • Row 等价于 Flex({ direction: FlexDirection.Row })

理解了 Flex,你就真正理解了 Column 和 Row 背后的运作机制。本文将通过一个完整的交互式示例应用,带你从零掌握 Flex 弹性布局。

二、Flex 的核心概念

在进入代码之前,我们需要先建立 Flex 布局的几个核心心智模型。

2.1 主轴与交叉轴

Flex 布局建立在两个轴的基础之上:

  • 主轴(Main Axis):由 direction 属性决定的方向。子组件沿着主轴依次排列。
  • 交叉轴(Cross Axis):与主轴垂直的方向。

directionFlexDirection.Row 时:

  • 主轴:水平方向(从左到右)
  • 交叉轴:垂直方向(从上到下)

directionFlexDirection.Column 时:

  • 主轴:垂直方向(从上到下)
  • 交叉轴:水平方向(从左到右)

一句话记住:当你使用 Row 时,主轴是水平的;当你使用 Column 时,主轴是垂直的。Flex 通过一个 direction 参数让你在这两者之间自由切换。

2.2 justifyContent 与 alignItems

这两个属性是 Flex 布局的两大核心控制参数:

属性 作用域 作用
justifyContent 主轴方向 控制子组件在主轴上的排列方式
alignItems 交叉轴方向 控制子组件在交叉轴上的对齐方式

justifyContent 的可选值:

枚举值 效果
FlexAlign.Start 从主轴起始位置开始排列(默认值)
FlexAlign.Center 在主轴上居中排列
FlexAlign.End 从主轴末尾位置开始排列
FlexAlign.SpaceBetween 两端对齐,项目之间的间隔都相等
FlexAlign.SpaceAround 每个项目两侧的间隔相等
FlexAlign.SpaceEvenly 项目之间的间隔与项目到容器的间隔都相等

alignItems 的可选值:

枚举值 Row 下效果 Column 下效果
ItemAlign.Start 顶部对齐 左侧对齐
ItemAlign.Center 垂直居中 水平居中
ItemAlign.End 底部对齐 右侧对齐
ItemAlign.Stretch 拉伸填满高度 拉伸填满宽度

2.3 为什么 Column 和 Row 不够用?

Column 和 Row 的问题是它们固定了主轴方向。如果你需要根据设备方向、用户偏好或数据状态来切换布局的排列方向,使用 Column 或 Row 就意味着你要在代码里做条件判断,在两个容器之间切换:

// 使用 Column/Row 需要条件切换
if (isHorizontal) {
  Row() { /* 子组件 */ }
} else {
  Column() { /* 子组件 */ }
}

// 使用 Flex 只需改变一个参数
Flex({ direction: isHorizontal ? FlexDirection.Row : FlexDirection.Column }) {
  /* 子组件 */
}

显然,后者更简洁、更易维护。这只是 Flex 优势的一个缩影。

三、示例应用功能预览

为了让你直观理解 Flex 的运作方式,我们构建了一个交互式演示页面。它包含以下功能模块:

3.1 状态信息面板

页面顶部有一个橙色的信息面板,实时显示当前的布局配置:

方向:Row(水平)
主轴对齐 (justifyContent):Center(居中)
交叉轴对齐 (alignItems):Center(居中)

当你点击下方的任何按钮修改配置时,这个面板会立即更新,让你始终清楚当前处于什么布局状态。

3.2 核心演示区

一个浅灰色背景的容器,内有三个不同颜色的方块(红色 A、绿色 B、蓝色 C)。这个容器就是我们的 Flex 组件,它的布局行为会随着下方的控制按钮实时变化。

  • 当你点击「Row(水平)」时,三个方块从左到右水平排列
  • 当你点击「Column(垂直)」时,三个方块从上到下垂直排列
  • 当你修改 justifyContent 时,方块在主轴上的间距和对齐方式随之变化
  • 当你修改 alignItems 时,方块在交叉轴上的位置随之变化

3.3 控制按钮区

页面下半部分提供了三组控制按钮:

  1. 方向切换:两个按钮,Row(水平)和 Column(垂直)
  2. 主轴对齐:六个按钮,Start / Center / End / SpaceBetween / SpaceAround / SpaceEvenly
  3. 交叉轴对齐:四个按钮,Start / Center / End / Stretch

每个按钮在被选中时会变为橙色高亮,未选中时保持白色边框,让用户一目了然。

四、完整代码逐段解析

下面是完整的 Index.ets 文件代码。我们分段来解析每一部分的设计意图和布局要点。

4.1 文件头部注释

/**
 * Flex 弹性布局入门 —— Column 与 Row 的统一抽象
 * ====================================================
 *
 * 核心概念:
 *   Flex 是 Column(纵向)和 Row(横向)的底层实现。
 *   换句话说,Column 等价于 Flex({ direction: FlexDirection.Column })
 *   Row    等价于 Flex({ direction: FlexDirection.Row })
 *
 * 通过 direction 参数,同一套 Flex 组件即可自由切换主轴方向,
 * 无需在 Column 和 Row 之间来回替换。
 *
 * 关键属性:
 *   - FlexDirection.Row        → 主轴水平(类似 Row)
 *   - FlexDirection.Column     → 主轴垂直(类似 Column)
 *   - FlexDirection.RowReverse → 主轴水平反向
 *   - FlexDirection.ColumnReverse → 主轴垂直反向
 *
 *   justifyContent(主轴对齐):
 *     FlexAlign.Start / Center / End / SpaceBetween / SpaceAround / SpaceEvenly
 *
 *   alignItems(交叉轴对齐):
 *     ItemAlign.Start / Center / End / Stretch
 */

这段注释不只是文档,更是学习提纲。它告诉读者:如果你读完了整篇文章,你应该能够回答以下几个问题:

  1. Flex 和 Column/Row 是什么关系?
  2. 通过哪个参数控制主轴方向?
  3. justifyContent 和 alignItems 各自控制哪个轴?
  4. 每个属性有哪些可选值?

把核心结论写在最前面,是技术写作中"结论先行"的好习惯。

4.2 子组件:DemoItem

@Component
struct DemoItem {
  /** 方块颜色(public 以便从父组件构造函数传入) */
  public color: Color = Color.Blue;
  /** 方块上显示的文本(public 以便从父组件构造函数传入) */
  public label: string = '';

  build() {
    Column() {
      Text(this.label)
        .fontColor(Color.White)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
    }
    .width(60)
    .height(60)
    .backgroundColor(this.color)
    .borderRadius(8)
    .shadow({ radius: 4, color: '#55000000', offsetX: 2, offsetY: 2 })
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

要点分析

  • 属性访问权限colorlabel 声明为 public。这是因为在 ArkTS 中,父组件通过构造器语法 DemoItem({ color: Color.Red, label: 'A' }) 传入的参数只能初始化 public 或带有 @State 装饰器的属性。如果这里是 private,编译会报错。这是 ArkTS 与标准 TypeScript 的一个重要区别,很多新手会在这里踩坑。
  • 尺寸固定:每个方块宽高都是 60vp(vp 是鸿蒙的虚拟像素单位,类似于 Android 的 dp),这样在演示 Flex 对齐效果时,子项的尺寸是确定的,变化的是它们在容器中的排列方式。
  • 圆角与阴影.borderRadius(8).shadow(...) 给方块增加了视觉层次感,让演示更有质感。

值得一提的是:ArkTS 要求 build() 方法中不能有匿名对象、箭头函数之外的复杂表达式,这在后续的状态绑定中也会体现。

4.3 状态变量声明

@Entry
@Component
struct Index {
  /** 当前 Flex 主轴方向,默认为 Row(水平排列) */
  @State private flexDirection: FlexDirection = FlexDirection.Row;

  /** 当前主轴对齐方式,默认为 Start(起始对齐) */
  @State private mainAlign: FlexAlign = FlexAlign.Start;

  /** 当前交叉轴对齐方式,默认为 Center(居中) */
  @State private crossAlign: ItemAlign = ItemAlign.Center;

要点分析

  • @State 装饰器:这是 ArkTS 响应式编程的核心。被 @State 修饰的属性一旦发生变化,框架会自动重新渲染与之关联的 UI 部分。当我们点击按钮修改 flexDirectionmainAligncrossAlign 时,Flex 容器和状态信息面板都会自动更新。

  • 命名冲突的坑direction 是一个常见的命名选择,但在 ArkTS 中,direction 与基类 CustomComponent 的一个属性重名,会导致编译错误:

    Property 'direction' in type 'Index' is not assignable to the same
    property in base type 'CustomComponent'.
    

    因此我们将变量命名为 flexDirection 来避免冲突。这是一个非常容易踩到的坑,值得特别留意。

  • 枚举类型的全局可用性FlexDirectionFlexAlignItemAlign 都是 ArkUI 框架内置的枚举类型,在 .ets 文件中全局可用,不需要额外的 import 语句。这与 HarmonyOS NEXT "Kit"化的模块组织方式保持一致。

4.4 枚举值转中文名称

  private getDirName(dir: FlexDirection): string {
    if (dir === FlexDirection.Row) { return 'Row(水平)'; }
    if (dir === FlexDirection.Column) { return 'Column(垂直)'; }
    return 'Row';
  }

  private getMainAlignName(align: FlexAlign): string {
    if (align === FlexAlign.Start) { return 'Start(起始)'; }
    if (align === FlexAlign.Center) { return 'Center(居中)'; }
    if (align === FlexAlign.End) { return 'End(末尾)'; }
    if (align === FlexAlign.SpaceBetween) { return 'SpaceBetween(两端对齐)'; }
    if (align === FlexAlign.SpaceAround) { return 'SpaceAround(环绕均分)'; }
    if (align === FlexAlign.SpaceEvenly) { return 'SpaceEvenly(等距均分)'; }
    return 'Start';
  }

要点分析

你可能注意到,这里没有使用对象映射(如 { [枚举值]: '中文名' })的写法,而是用了连续的 if 判断。这不是风格偏好,而是 ArkTS 编译器的硬性限制

ArkTS 是 TypeScript 的一个严格子集,它禁用了不少灵活但容易出错的语法特性。具体来说:

  • 不支持计算属性名{ [FlexDirection.Row]: 'Row(水平)' } 这种写法中的方括号计算属性名不被允许,会报 Objects with property names that are not identifiers are not supported
  • 不支持匿名对象字面量:对象字面量必须对应某个显式声明的类或接口,否则会报 Object literal must correspond to some explicitly declared class or interface

换句话说,你不能随手写一个 const map = { [enumA]: 'a', [enumB]: 'b' } 然后用 map[value] 来查表。必须用 if-else 或者创建一个正式的类/接口来承载映射关系。

这个限制有时会让习惯了标准 TypeScript 的开发者感到不便,但它换来了运行时性能的提升和更严格的类型安全。

4.5 核心:Flex 容器

      Flex({
        direction: this.flexDirection,
        justifyContent: this.mainAlign,
        alignItems: this.crossAlign,
      }) {
        DemoItem({ color: Color.Red, label: 'A' })
        DemoItem({ color: Color.Green, label: 'B' })
        DemoItem({ color: Color.Blue, label: 'C' })
      }
      .width('90%')
      .height(200)
      .backgroundColor('#EEEEEE')
      .borderRadius(12)
      .padding(8)
      .margin({ bottom: 20 })

要点分析

这是整个页面的核心代码,一共只有 7 行(含花括号)。它做了以下几件事:

  1. 创建 Flex 容器:通过 Flex({...}) 的构造参数传入了三个关键属性:

    • direction:绑定到 this.flexDirection 状态变量
    • justifyContent:绑定到 this.mainAlign 状态变量
    • alignItems:绑定到 this.crossAlign 状态变量
  2. 放入三个子组件:在 Flex 的尾随 lambda 闭包中依次放置三个 DemoItem 实例。

  3. 设置容器样式

    • width('90%'):占父容器宽度的 90%
    • height(200):固定 200vp 的高度,这样当 alignItems 变化时,子项在交叉轴上的位置变化才看得清楚
    • backgroundColor('#EEEEEE'):浅灰色背景,清晰地标出 Flex 容器的边界范围
    • borderRadius(12)padding(8):让容器看起来更柔和

动态演示的核心机制:当用户点击 “Row” 按钮时,this.flexDirection 被设置为 FlexDirection.Row;点击 “Column” 时,设置为 FlexDirection.Column@State 装饰器监听到值变化后,自动触发当前组件树的重新渲染。Flex 容器读取最新的 direction 值,重新排列子项——整个过程无需手动操作 DOM,也无需手动刷新。这就是声明式 UI 框架的典型工作流。

4.6 主轴对齐按钮组

      Flex({
        direction: FlexDirection.Row,
        wrap: FlexWrap.Wrap,
        justifyContent: FlexAlign.Center,
      }) {
        Button('Start')
          .onClick(() => { this.mainAlign = FlexAlign.Start; })
          .btnStyle(this.mainAlign === FlexAlign.Start)
        Button('Center')
          .onClick(() => { this.mainAlign = FlexAlign.Center; })
          .btnStyle(this.mainAlign === FlexAlign.Center)
        // ... 省略其他按钮
      }

要点分析

这段代码展示了 Flex 的另一个重要特性:flexWrap

  • wrap: FlexWrap.Wrap:当子项在一行内排不下时,自动换行到下一行。考虑到手机屏幕宽度有限,而我们有 6 个按钮,开启换行是必要的——否则按钮可能会被挤压到不可读的宽度。
  • 方法链的调用顺序@Extend 装饰的函数(如 btnStyle)返回 void,因此后续不能再链式调用 .onClick()。正确的做法是让 onClickbtnStyle 之前调用,这样 Button(...) 返回 Button 实例,.onClick() 也返回 Button 实例,最后 .btnStyle() 作为链尾。

4.7 扩展样式:@Extend

@Extend(Button)
function btnStyle(active: boolean): void {
  .fontSize(13)
  .fontColor(active ? Color.White : '#333333')
  .backgroundColor(active ? '#FF6F00' : '#FFFFFF')
  .border({ width: 1, color: active ? '#FF6F00' : '#CCCCCC' })
  .height(34)
  .margin(4)
}

@Extend 是 ArkTS 提供的样式复用机制。它的作用类似于 CSS 中的类选择器——定义一个样式集合,然后在多个组件上复用。

这里,btnStyle 根据 active 参数来决定按钮的外观:

  • 选中态(active = true):白色文字、橙色背景、橙色边框
  • 未选中态(active = false):深灰文字、白色背景、灰色边框

五、ArkTS 与标准 TypeScript 的关键差异

在编写上述代码的过程中,我们遇到了多个 ArkTS 的限制。这里做一个小结,帮助习惯标准 TS 的读者快速适应。

特性 标准 TypeScript ArkTS
计算属性名 支持 { [expr]: val } 不支持
匿名对象字面量 随意使用 必须对应声明过的类/接口
组件属性的构造器初始化 无限制 只能初始化 public@State 属性
@Extend 的返回值 可返回组件实例 返回 void,不能继续链式调用
枚举的导入 需要 import 框架内置枚举全局可用
属性名冲突 开发者自行注意 与基类属性同名会编译报错

六、Flex 在真实项目中的应用场景

学完了基础语法,你可能会问:在实际项目中,Flex 到底用在哪些地方?

6.1 自适应工具栏

在一个应用中,工具栏的按钮数量可能随时间变化。使用 Flex + FlexWrap.Wrap 可以实现按钮的自动换行布局,无需手动计算位置。

Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
  Button('复制').onClick(() => {})
  Button('粘贴').onClick(() => {})
  Button('剪切').onClick(() => {})
  Button('删除').onClick(() => {})
  Button('重命名').onClick(() => {})
  Button('分享').onClick(() => {})
  Button('更多').onClick(() => {})
}

当屏幕宽度足够时,所有按钮在一行内显示;宽度不足时,后面的按钮自动换行到下一行。

6.2 卡片列表

在社交媒体或电商应用中,经常需要展示一个卡片列表。Flex 配合 SpaceBetween 可以让卡片在主轴方向上均匀分布:

Flex({
  direction: FlexDirection.Row,
  justifyContent: FlexAlign.SpaceBetween,
  alignItems: ItemAlign.Start,
}) {
  ProductCard({ item: product1 })
  ProductCard({ item: product2 })
  ProductCard({ item: product3 })
}

6.3 垂直居中的弹窗

弹窗内容在屏幕中垂直居中是最常见的需求之一。使用 Flex + Column 可以实现完美居中:

Flex({
  direction: FlexDirection.Column,
  justifyContent: FlexAlign.Center,
  alignItems: ItemAlign.Center,
}) {
  Text('提示信息')
  Button('确定')
}

6.4 动态切换方向的布局

在某些场景中,用户的操作需要改变布局方向——比如点击一个"切换布局"按钮,让列表在横向和纵向之间切换。使用 Flex,只需要改变一个参数:

@State private isHorizontal: boolean = true;

build() {
  Flex({
    direction: this.isHorizontal ? FlexDirection.Row : FlexDirection.Column,
    justifyContent: FlexAlign.Center,
    alignItems: ItemAlign.Center,
  }) {
    ItemView({ data: item1 })
    ItemView({ data: item2 })
    ItemView({ data: item3 })
  }
}

这是 Column/Row 无法做到的——你必须用 Flex。

七、常见问题与最佳实践

7.1 什么时候用 Column/Row,什么时候直接用 Flex?

这是一个很实际的问题。我的建议是:

  • 确定的方向:如果布局方向是固定的且不会改变,使用 Column 或 Row。它们的 API 更简洁,语义更明确。
  • 动态的方向:如果方向会根据状态变化,或者需要精细控制 justifyContent 和 alignItems,直接用 Flex。
  • 需要换行:如果子项需要换行,必须用 Flex(Column/Row 不支持换行)。

记住:Column 和 Row 是"语法糖",Flex 是"完整功能版"。用哪一个取决于你需要的功能集。

7.2 alignItems 的 Stretch 模式为什么没生效?

ItemAlign.Stretch 会让子项在交叉轴方向上拉伸以填满容器。但如果子项在交叉轴方向上有明确的尺寸设定(如 widthheight),Stretch 将不会生效。

具体来说:

  • 在 Row(主轴水平)模式下,交叉轴是垂直方向。Stretch 会拉伸子项的 height,但如果子项已设置明确 height,拉伸无效。
  • 在 Column(主轴垂直)模式下,交叉轴是水平方向。Stretch 会拉伸子项的 width,但如果子项已设置明确 width,拉伸无效。

7.3 为什么我的 Flex 容器没有高度?

Flex 容器默认的高度由其子项撑开。如果你希望看到 alignItems 的效果(特别是 Center 和 Stretch),需要给 Flex 容器设置一个明确的高度(如 height(200)),否则交叉轴的空间就是子项本身的高度,对齐效果看不出来。

7.4 性能注意事项

Flex 布局的计算量相比 Column/Row 会稍大一些,因为 Flex 需要处理更多的排列逻辑(换行、对齐计算等)。在绝大多数场景下,这种差异是可以忽略不计的。但如果你的页面有上千个子项需要频繁重排,建议:

  1. 优先使用 LazyForEach 进行虚拟列表渲染
  2. 避免频繁切换 FlexDirectionjustifyContent(每次切换都会触发 relayout)
  3. 如果不需要换行,可以考虑用 Column/Row 替代 Flex

八、延伸学习:Flex 的进阶功能

本文展示的只是 Flex 布局的基础用法。在实际开发中,Flex 还提供了更多强大功能:

8.1 flexShrink 与 flexGrow

  • flexShrink:当容器空间不足时,子项的缩小比例。默认值为 1(平均缩小)。设置为 0 表示不缩小。
  • flexGrow:当容器有剩余空间时,子项的放大比例。默认值为 0(不放大)。设置为 1 表示等分剩余空间。

8.2 alignSelf

alignSelf 允许子项覆盖父容器 alignItems 的设置,实现单个子项的独立对齐。

Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
  Text('居中')
  Text('到底部').alignSelf(ItemAlign.End)
  Text('到顶部').alignSelf(ItemAlign.Start)
}

8.3 FlexDirection 的反向模式

  • FlexDirection.RowReverse:水平反向排列(从右到左)
  • FlexDirection.ColumnReverse:垂直反向排列(从下到上)

九、总结

通过本文的讲解和示例代码,我们完成了 Flex 弹性布局的入门学习。回顾一下核心知识点:

  1. Flex 的本质:Flex 是 Column 和 Row 的底层统一抽象。Column 等价于 Flex({ direction: FlexDirection.Column }),Row 等价于 Flex({ direction: FlexDirection.Row })

  2. 主轴与交叉轴direction 控制主轴方向,justifyContent 控制主轴对齐,alignItems 控制交叉轴对齐。

  3. 响应式状态:结合 @State 装饰器,Flex 的布局属性可以动态变化并自动触发 UI 刷新。

  4. ArkTS 限制:与标准 TypeScript 相比,ArkTS 有一些限制(计算属性名、匿名对象、@Extend 返回值等),需要在编码时留意。

  5. 最佳实践:方向固定用 Column/Row,方向动态或需要细粒度控制用 Flex,需要换行必须用 Flex。

Flex 弹性布局是 HarmonyOS 应用开发中最基础也最重要的布局技能之一。掌握了 Flex,你不仅能理解 Column 和 Row 的底层机制,还能写出更灵活、更易于维护的页面布局代码。

十、完整源代码

你可以在以下路径找到完整的源代码文件:

entry/src/main/ets/pages/Index.ets

运行此应用需要:

  • DevEco Studio 5.0+
  • HarmonyOS NEXT API 12+(兼容 SDK 6.1.0+)
  • 真机或模拟器

建议在 DevEco Studio 中创建新项目后,将 Index.ets 文件替换为本文提供的代码,然后运行查看效果。通过点击不同的按钮,直观感受 Flex 布局参数变化对子项排列产生的影响。


关于作者:本文是"HarmonyOS 原生 ArkTS 布局系列"的第一篇。后续文章将深入探讨 Grid 网格布局、Stack 层叠布局、RelativeContainer 相对布局等主题。如果你有任何问题或建议,欢迎在评论区留言讨论。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐