【共创季稿事节】鸿蒙原生 ArkTS 布局方式之 Flex 弹性布局入门:Column 与 Row 的统一抽象
鸿蒙原生 ArkTS 布局方式之 Flex 弹性布局入门:Column 与 Row 的统一抽象




一、前言:为什么你需要理解 Flex?
如果你是第一次接触 HarmonyOS 原生应用开发,那么你一定会频繁遇到两个基础布局容器——Column 和 Row。前者将子组件沿垂直方向(主轴从上到下)排列,后者将子组件沿水平方向(主轴从左到右)排列。在绝大多数页面场景中,这两个组件确实够用了。但当你开始遇到更复杂的布局需求——比如动态切换排列方向、主轴与交叉轴的精细对齐、子项换行与缩放——你会发现 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):与主轴垂直的方向。
当 direction 为 FlexDirection.Row 时:
- 主轴:水平方向(从左到右)
- 交叉轴:垂直方向(从上到下)
当 direction 为 FlexDirection.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 控制按钮区
页面下半部分提供了三组控制按钮:
- 方向切换:两个按钮,Row(水平)和 Column(垂直)
- 主轴对齐:六个按钮,Start / Center / End / SpaceBetween / SpaceAround / SpaceEvenly
- 交叉轴对齐:四个按钮,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
*/
这段注释不只是文档,更是学习提纲。它告诉读者:如果你读完了整篇文章,你应该能够回答以下几个问题:
- Flex 和 Column/Row 是什么关系?
- 通过哪个参数控制主轴方向?
- justifyContent 和 alignItems 各自控制哪个轴?
- 每个属性有哪些可选值?
把核心结论写在最前面,是技术写作中"结论先行"的好习惯。
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)
}
}
要点分析:
- 属性访问权限:
color和label声明为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 部分。当我们点击按钮修改flexDirection、mainAlign或crossAlign时,Flex 容器和状态信息面板都会自动更新。 -
命名冲突的坑:
direction是一个常见的命名选择,但在 ArkTS 中,direction与基类CustomComponent的一个属性重名,会导致编译错误:Property 'direction' in type 'Index' is not assignable to the same property in base type 'CustomComponent'.因此我们将变量命名为
flexDirection来避免冲突。这是一个非常容易踩到的坑,值得特别留意。 -
枚举类型的全局可用性:
FlexDirection、FlexAlign、ItemAlign都是 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 行(含花括号)。它做了以下几件事:
-
创建 Flex 容器:通过
Flex({...})的构造参数传入了三个关键属性:direction:绑定到this.flexDirection状态变量justifyContent:绑定到this.mainAlign状态变量alignItems:绑定到this.crossAlign状态变量
-
放入三个子组件:在
Flex的尾随 lambda 闭包中依次放置三个DemoItem实例。 -
设置容器样式:
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()。正确的做法是让onClick在btnStyle之前调用,这样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 会让子项在交叉轴方向上拉伸以填满容器。但如果子项在交叉轴方向上有明确的尺寸设定(如 width 或 height),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 需要处理更多的排列逻辑(换行、对齐计算等)。在绝大多数场景下,这种差异是可以忽略不计的。但如果你的页面有上千个子项需要频繁重排,建议:
- 优先使用
LazyForEach进行虚拟列表渲染 - 避免频繁切换
FlexDirection和justifyContent(每次切换都会触发 relayout) - 如果不需要换行,可以考虑用 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 弹性布局的入门学习。回顾一下核心知识点:
-
Flex 的本质:Flex 是 Column 和 Row 的底层统一抽象。Column 等价于
Flex({ direction: FlexDirection.Column }),Row 等价于Flex({ direction: FlexDirection.Row })。 -
主轴与交叉轴:
direction控制主轴方向,justifyContent控制主轴对齐,alignItems控制交叉轴对齐。 -
响应式状态:结合
@State装饰器,Flex 的布局属性可以动态变化并自动触发 UI 刷新。 -
ArkTS 限制:与标准 TypeScript 相比,ArkTS 有一些限制(计算属性名、匿名对象、@Extend 返回值等),需要在编码时留意。
-
最佳实践:方向固定用 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 相对布局等主题。如果你有任何问题或建议,欢迎在评论区留言讨论。
更多推荐




所有评论(0)