【共创季稿事节】鸿蒙原生ArkTS布局方式之Flex+flexBasis基准尺寸布局
鸿蒙原生ArkTS布局方式之Flex+flexBasis基准尺寸布局

一、引言
在鸿蒙 HarmonyOS NEXT 应用开发中,布局是构建用户界面的基石。ArkTS 作为鸿蒙原生声明式开发语言,提供了丰富的布局容器组件,其中 Flex(弹性布局) 是最核心、最灵活的布局方案之一。Flex 布局借鉴了 CSS Flexbox 的设计思想,同时针对鸿蒙系统的特点进行了原生适配和优化。
flexBasis 是 Flex 布局中一个关键但又容易被忽视的属性。它决定了弹性子组件在「分配弹性空间之前」的初始主轴尺寸,是整个弹性布局计算过程的起点。深入理解 flexBasis 的原理和使用场景,能够帮助我们更精准地控制界面布局,避免出现意料之外的尺寸跳动或布局错乱。
本文将以实际代码为例,从基础概念到高级用法,全面剖析 Flex + flexBasis 布局方式。文章配套的完整示例代码已经过编译验证(零错误、零警告),可直接在 DevEco Studio 中运行观察。
二、Flex 弹性布局基础
2.1 什么是 Flex 布局
Flex 布局(Flexible Box Layout,弹性盒布局)是一种一维布局模型,它允许容器内的子组件在主轴方向上灵活排列,并根据可用空间动态调整尺寸。在 ArkTS 中,Flex 布局通过 Flex 容器组件实现。
与传统的线性布局(Row、Column)相比,Flex 布局提供了更丰富的弹性控制能力:
- Row 布局:子组件按水平方向排列,不支持换行,没有弹性伸缩能力。每个子组件的宽度由自身决定或由父容器约束。
- Column 布局:子组件按垂直方向排列,同样不支持换行,没有弹性伸缩能力。
- Flex 布局:同时具备 Row 和 Column 的方向控制能力(通过 direction 参数切换),更重要的是提供了 flexBasis、flexGrow、flexShrink 三个弹性属性,让子组件能够根据可用空间动态调整尺寸。
可以这样理解:Row 和 Column 是「刚性」布局——子组件有多大就占多大位置,超出就溢出;而 Flex 是「弹性」布局——子组件可以伸缩,自动适应容器的大小变化。这种弹性能力在屏幕尺寸多样化的移动端设备上尤为重要。
2.2 Flex 容器基本属性
在 ArkTS 中创建一个 Flex 容器非常直观:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
// 子组件列表
}
构造函数接受一个 FlexOptions 参数,主要属性包括:
- direction:主轴方向(
FlexDirection.Row水平 |FlexDirection.Column垂直) - wrap:是否换行(
FlexWrap.NoWrap不换行 |FlexWrap.Wrap换行) - justifyContent:主轴对齐方式
- alignItems:交叉轴对齐方式
- alignContent:多行时的交叉轴对齐方式
2.3 弹性子组件的关键属性
Flex 容器中的每个子组件都可以设置以下弹性属性:
| 属性 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| flexBasis | number | string | 0(或 auto) | 初始主轴尺寸 |
| flexGrow | number | 0 | 拉伸比例 |
| flexShrink | number | 1 | 压缩比例 |
这三个属性共同决定了子组件在 Flex 容器中的最终尺寸。
三、flexBasis 核心概念详解
3.1 flexBasis 的定义
flexBasis 指定了弹性子组件在「分配弹性空间之前」的初始主轴尺寸。简化理解就是:
- 在 Row(水平方向) 布局中,flexBasis 替代了 width 的作用
- 在 Column(垂直方向) 布局中,flexBasis 替代了 height 的作用
为什么需要 flexBasis 而不是直接使用 width 或 height 呢?原因在于弹性布局的「弹性」二字。假设我们有一个水平方向的 Flex 容器,里面有三个子组件,分别设置了 width 为 80、80 和 200。如果容器的总宽度是 360,那么三个子组件加起来就是 360,刚好填满。但是——如果容器宽度变成 400 呢?多出来的 40 归谁?如果容器宽度变成 300 呢?少掉的 60 谁来承担?
在非弹性布局中,这些问题没有好的答案。组件要么溢出,要么留白。而在 Flex 弹性布局中,flexBasis 给出了一个优雅的解决方案:
- flexBasis 定义「初始」尺寸,而不是「最终」尺寸。
- 如果容器有剩余空间,由 flexGrow 决定谁去「吸收」这些空间。
- 如果容器空间不足,由 flexShrink 决定谁去「压缩」来腾出空间。
这就是 flexBasis 不可替代的原因——它不只是一个尺寸设定,而是一个弹性计算的起点。
3.2 flexBasis 支持的取值类型
在 ArkTS 中,flexBasis 接受 number 或 string 类型的参数:
① 数值(默认单位为 vp)
.flexBasis(80) // 主轴方向初始尺寸为 80vp
.flexBasis(120) // 主轴方向初始尺寸为 120vp
② 百分比字符串
.flexBasis('30%') // 主轴方向初始尺寸为容器可用空间的 30%
.flexBasis('50%') // 主轴方向初始尺寸为容器可用空间的 50%
③ ‘auto’ 关键字
.flexBasis('auto') // 基准尺寸由子组件自身内容决定
3.3 flexBasis 与 width/height 的关系
这是理解 flexBasis 最关键的要点:
- 当 Flex 容器的主轴方向为 Row 时:flexBasis 覆盖 width。即使子组件同时设置了
.width(100)和.flexBasis(80),最终的主轴尺寸也由 flexBasis 决定。 - 当 Flex 容器的主轴方向为 Column 时:flexBasis 覆盖 height。
- 当 flexBasis 设置为
'auto'时,子组件回退到使用 width(Row)或 height(Column)作为基准。
3.4 弹性空间分配流程
Flex 布局的尺寸计算遵循一个清晰的三步流程:
步骤 1:按 flexBasis 分配初始尺寸
↓
步骤 2:计算剩余空间(容器大小 - 所有 flexBasis 之和 - 间距)
↓
步骤 3:按 flexGrow / flexShrink 分配剩余或超出的空间
具体而言:
- 如果
所有 flexBasis 之和 < 容器尺寸,产生剩余空间,由 flexGrow > 0 的子项按比例吸收。 - 如果
所有 flexBasis 之和 > 容器尺寸,产生超出空间,由 flexShrink > 0 的子项按比例压缩。 - flexGrow 和 flexShrink 的默认值分别为 0 和 1。
四、六大演示场景深度解析
以下逐一剖析示例代码中的六个演示区,每个场景都对应一种典型的 flexBasis 使用模式。
演示场景 ①:等分基准
代码:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '80vp', isLast: true })
.flexBasis(80)
}
.width(360)
.height(60)
布局计算过程:
| 项目 | 值 |
|---|---|
| 容器宽度 | 360vp |
| 容器内边距(左右各 4vp) | 8vp |
| 子项间距(8vp × 2) | 16vp |
| 可用空间 | 360 - 8 - 16 = 336vp |
| 各子项 flexBasis | 80vp |
| 各子项初始尺寸总和 | 240vp |
| 剩余空间 | 336 - 240 = 96vp |
| 剩余空间分配方式 | 由于 flexGrow 默认均为 0,不拉伸 |
| 最终每个子项宽度 | 80vp(剩余空间留在右侧) |
视觉特点: 三个色块宽度均为 80vp,容器右侧留有空白区域。这个演示清晰地展示了「固定基准 + 不拉伸」的效果。
适用场景:
- 工具栏中固定宽度的按钮组、图标列表。
- 底部导航栏中均分的选项按钮(如微信底部的四个 Tab)。
- 日历组件中的日期格子,每个日期占据相同的宽度。
需要特别说明的是,这种「等分但留有余地」的布局方式在实际项目中非常常见。因为很多设计师希望子项之间有固定的间距(gap),而间距占据了部分空间,所以子项最终并不是严格等分容器总宽度。如果您希望子项严格等分容器宽度(即不留右侧空隙),需要配合 flexGrow 使用,我们在演示场景④中会看到这种用法。
演示场景 ②:混合基准
代码:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '100vp' })
.flexBasis(100)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '120vp', isLast: true })
.flexBasis(120)
}
.width(360)
.height(60)
布局计算过程:
| 项目 | 值 |
|---|---|
| 可用空间 | 336vp(计算方式同上) |
| A 的 flexBasis | 100vp |
| B 的 flexBasis | 80vp |
| C 的 flexBasis | 120vp |
| 初始总和 | 300vp |
| 剩余空间 | 336 - 300 = 36vp(留在右侧) |
视觉特点: 三个色块宽度各不相同(100vp、80vp、120vp),精确体现了各自 flexBasis 的设定值。右侧仍有少量剩余空间未被使用。
适用场景: 表单中不同宽度的输入框、侧边栏中不同宽度的菜单项、表格列宽分配。
演示场景 ③:百分比基准
代码:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '30%' })
.flexBasis('30%')
SpacedBlock({ label: 'B', color: '#67C23A', basis: '40%' })
.flexBasis('40%')
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '30%', isLast: true })
.flexBasis('30%')
}
.width(360)
.height(60)
关键说明:
百分比值是相对于容器的可用空间(即扣除 padding 后的剩余宽度)计算的,而不是相对容器总宽度。
可用空间 = 360 - 8(padding) = 352vp
A = 352 × 30% ≈ 105.6vp
B = 352 × 40% ≈ 140.8vp
C = 352 × 30% ≈ 105.6vp
合计 ≈ 352vp(恰好填满)
视觉特点: 容器被完美三等分(30% + 40% + 30% = 100%),没有剩余空间,也没有溢出。
适用场景:
- 响应式布局中需要按比例分配空间的场景,例如仪表盘中的指标卡片、图表区域分割。
- 登录页面中「用户名输入框」和「密码输入框」各占一半宽度。
- 商品详情页中「价格」「原价」「折扣」三列按比例排列。
- 设置页面中「开关标签」和「开关控件」按 70% : 30% 的比例分配行宽。
注意陷阱: 如果百分比之和超过 100%,子项会溢出容器;如果不足 100%,则留有剩余空间(由 flexGrow 决定是否拉伸)。
百分比基准布局的一个显著优势是「响应式」。假设您的应用需要在平板和手机上都能良好显示,使用百分比值后,子组件的宽度会自动按比例缩放,无需为不同屏幕尺寸编写多套布局代码。这是现代移动应用开发中非常推崇的做法。
演示场景 ④:flexBasis + flexGrow 弹性增长
代码:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '80vp', isLast: true })
.flexBasis(80)
.flexGrow(this.growValue) // 滑块调节 0~5
.flexShrink(0) // 禁止压缩
}
.width(360)
.height(60)
这是最核心的演示场景,它展示了 flexBasis 如何与 flexGrow 协同工作。
当 growValue = 1 时:
| 项目 | 值 |
|---|---|
| 可用空间 | 336vp |
| A、B、C 的 flexBasis | 各 80vp,总计 240vp |
| 剩余空间 | 336 - 240 = 96vp |
| flexGrow 分配 | A=0, B=0, C=1 |
| C 吸收的剩余空间 | 96 × (1/(0+0+1)) = 96vp |
| C 的最终宽度 | 80 + 96 = 176vp |
当 growValue = 2 时:
| 项目 | 值 |
|---|---|
| flexGrow 分配 | A=0, B=0, C=2 |
| C 吸收的剩余空间 | 96 × (2/(0+0+2)) = 96vp |
| C 的最终宽度 | 80 + 96 = 176vp |
当 growValue = 0 时:
| 项目 | 值 |
|---|---|
| flexGrow 分配 | A=0, B=0, C=0 |
| C 吸收的剩余空间 | 96 × 0 = 0vp |
| C 的最终宽度 | 80vp(与 A、B 相同) |
交互体验: 页面中提供了一个 Slider 滑块组件,可动态调节 C 的 flexGrow 值(0→5)。当 grow 为 0 时,C 与 A、B 宽度相同;当 grow ≥ 1 时,C 吸收所有剩余空间,变得更宽;由于 A 和 B 的 flexGrow 始终为 0,它们的宽度保持 80vp 不变。
关于 flexShrink(0) 的说明: 我们为 C 设置了 flexShrink(0),这意味着即使在极端情况下(容器变得很窄),C 也不会被压缩到低于 80vp。这在自适应布局中非常有用——确保某个关键元素保持最小宽度。
flexGrow 与 flexShrink 的对称性: 初学者常常混淆 flexGrow 和 flexShrink 的工作方式。这里有一个简单的记忆方法——flexGrow 处理「有多的怎么分」,flexShrink 处理「不够了怎么压」。两者都是比例值,但作用的场景正好相反。在同一个弹性布局中,可能既有子项需要拉伸(flexGrow > 0),又有子项需要压缩(flexShrink > 0),这取决于容器空间与所有子项 flexBasis 总和之间的大小关系。
适用场景:
- 聊天界面:输入框固定高度,发送按钮 flexGrow 占满剩余空间
- 搜索栏:搜索图标固定宽度,输入框 flexGrow(1) 填满
- 导航栏:固定宽度的返回按钮 + 自适应宽度的标题 + 固定宽度的菜单按钮
- 个人中心页面:用户头像固定尺寸 + 用户信息 flexGrow 自适应宽度
- 商品列表:商品图片固定宽度 + 商品描述 flexGrow 自适应
演示场景 ⑤:垂直方向(Column)
代码:
Flex({ direction: FlexDirection.Column, wrap: FlexWrap.NoWrap }) {
ColorBlock({ label: 'A', color: '#409EFF', basis: '60vp' })
.flexBasis(60)
.margin({ bottom: 8 })
ColorBlock({ label: 'B', color: '#67C23A', basis: '40vp' })
.flexBasis(40)
.margin({ bottom: 8 })
ColorBlock({ label: 'C', color: '#E6A23C', basis: '80vp' })
.flexBasis(80)
.flexGrow(1)
}
.width(180)
.height(280)
关键变化: 当 direction 从 Row 切换为 Column 时,flexBasis 的作用对象从「宽度」切换为「高度」。
布局计算过程:
| 项目 | 值 |
|---|---|
| 容器高度 | 280vp |
| 容器内边距(上下各 6vp) | 12vp |
| 子项间距(8vp × 2) | 16vp |
| 可用空间 | 280 - 12 - 16 = 252vp |
| A 的 flexBasis | 60vp |
| B 的 flexBasis | 40vp |
| C 的 flexBasis | 80vp |
| 初始总和 | 180vp |
| 剩余空间 | 252 - 180 = 72vp |
| flexGrow 分配 | A=0, B=0, C=1 |
| C 的最终高度 | 80 + 72 = 152vp |
视觉特点: 在垂直方向上,A 和 B 保持固定高度(60vp 和 40vp),C 吸收了所有剩余垂直空间。这是一个典型的「固定头部 + 固定底部 + 自适应中间」布局模式。
适用场景:
- 文章详情页:标题区固定高度 + 正文内容 flexGrow 填满
- 列表页:顶部搜索栏固定 + 列表区域自适应 + 底部 TabBar 固定
- 模态弹窗:标题固定 + 内容自适应 + 按钮固定
演示场景 ⑥:auto 自适应基准
代码:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
Text('A:短文本')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#409EFF')
.borderRadius(8)
.padding(8)
.flexBasis('auto') // 宽度由文本内容决定
Text('B:这是一段较长的文本内容,我的宽度由内容决定')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#67C23A')
.borderRadius(8)
.padding(8)
.margin({ left: 8 })
.flexBasis('auto') // 宽度由文本内容决定
Text('C(弹性)')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#E6A23C')
.borderRadius(8)
.padding(8)
.margin({ left: 8 })
.flexBasis('auto') // 基准 = 内容宽度
.flexGrow(1) // 吸收剩余空间,填满右侧
}
.width(360)
核心原理:
flexBasis('auto') 告诉 Flex 容器:请以子组件自身内容的尺寸作为基准。具体来说:
- 对于 Text 组件,
'auto'基准就是文本的实际渲染宽度(包含 padding)。 - A 的宽度 = “A:短文本” 的渲染宽度 + 左右 padding(16vp)
- B 的宽度 = “B:这是一段较长的文本内容…” 的渲染宽度 + 左右 padding(16vp)
- C 的基础宽度 = “C(弹性)” 的渲染宽度 + 左右 padding(16vp),然后加上 flexGrow(1) 吸收剩余空间
视觉特点: A 和 B 的宽度完全取决于文本内容,互不影响。C 除了内容宽度外,还通过 flexGrow(1) 吸收了右侧剩余空间。这在内容长度不确定的国际化场景中尤其有用。
适用场景:
- 标签列表:标签宽度由文字长度决定
- 多语言界面:不同语言翻译后长度不同,flexBasis(‘auto’) 自动适配
- 用户昵称显示:昵称长度不确定,但希望保持紧凑
- 评论列表中的用户名和评论内容:用户名宽度由文字决定,评论内容 flexGrow 填满剩余空间
- 消息气泡:气泡宽度由消息文字长度决定,但不超过最大宽度约束
‘auto’ 与 ‘0’ 的关键区别: 在实际开发中,这是最容易混淆的两个概念。请牢记:
flexBasis('auto')= 「我有多大就多大,多余的空间我不主动要」flexBasis(0)= 「我不管内容有多少,我的基准是 0,全靠 flexGrow 分给我」
如果在一个 Flex 容器中,三个子项都设置为 flexBasis(0) 且 flexGrow(1),那么它们将严格三等分容器空间,不管各自的内容有多少。而如果设置为 flexBasis('auto') 且 flexGrow(1),那么它们会先按内容宽度占据空间,再均分剩余空间。这会导致内容多的子项最终比内容少的子项更大。理解这个区别至关重要,它决定您要做的是「等分布局」还是「内容自适应 + 弹性填充」。
五、flexBasis 与 flexGrow / flexShrink 的完整关系
5.1 三维属性模型
flexBasis、flexGrow、flexShrink 三个属性共同构成了弹性子组件的尺寸决策模型。它们的关系可以用以下伪代码概括:
最终尺寸 = flexBasis + 剩余空间 × (flexGrow / 所有 flexGrow 之和)
- 超出空间 × (flexShrink / 所有 flexShrink 之和)
5.2 关键公式
有剩余空间时(flexBasis 总和 < 容器空间):
子项 i 的最终尺寸 = flexBasis(i) + 剩余空间 × [flexGrow(i) / Σ flexGrow]
有超出空间时(flexBasis 总和 > 容器空间):
子项 i 的最终尺寸 = flexBasis(i) - 超出空间 × [flexShrink(i) / Σ flexShrink]
5.3 特殊情况处理
| 情况 | 行为 |
|---|---|
| 所有 flexGrow 均为 0 | 剩余空间保留,不分配 |
| 所有 flexShrink 均为 0 | 子项溢出容器,不压缩 |
| flexBasis 为 0 | 子项的初始尺寸为 0,完全由 flexGrow 分配 |
| flexBasis 为负数 | 按默认值处理(通常为 0 或 auto) |
5.4 layoutWeight 与 flexBasis 的区别
HarmonyOS 还提供了 layoutWeight 属性,它和 flexBasis + flexGrow 的组合在效果上有些相似,但存在本质区别:
| 对比维度 | flexBasis + flexGrow | layoutWeight |
|---|---|---|
| 初始基准 | flexBasis 提供 | 无基准,从 0 开始 |
| 空间分配 | 先按基准分配,再分配剩余 | 直接按权重分配全部空间 |
| 灵活性 | flexGrow + flexShrink 可分别控制 | 只有权重比例 |
| 适用场景 | 固定尺寸 + 弹性混合的场景 | 纯比例分配的场景 |
简单来说,layoutWeight 更适合「水果拼盘——按比例分完」的场景,而 flexBasis + flexGrow 更适合「自助餐——先拿固定份,再看剩余量分」的场景。
六、最佳实践与常见陷阱
6.1 最佳实践
① 混合固定与弹性: 使用 flexBasis 固定「不应伸缩」的部分,用 flexGrow 让「应该伸缩」的部分吸收剩余空间。这是 flexBasis 最强大的用法。
② 防止关键元素被压缩: 对于不希望被压缩的子项,始终设置 .flexShrink(0)。这在响应式布局中尤其重要。
③ 优先使用百分比而非固定数值: 当需要按比例分配时,百分比值比固定数值更具响应性。
④ Column 布局中同样适用: 不要只习惯在 Row 中使用 flexBasis,在 Column 中垂直分配空间同样是常用场景。
⑤ 配合 justify-content 使用: 当子项总宽度小于容器时,justifyContent 决定了剩余空间在子项之间的分配方式。注意当 flexGrow 生效时,剩余空间已经被子项吸收,justifyContent 不起作用。
6.2 常见陷阱
陷阱 1:忘记 flexGrow 默认值为 0
很多开发者误以为 Flex 容器会自动拉伸子项填满空间。实际上,如果不设置 flexGrow,子项保持 flexBasis 的初始尺寸,剩余空间不会被填充。
陷阱 2:flexBasis 与 width 同时设置时的优先级
在 Row 方向中,flexBasis 的优先级高于 width。如果同时设置了 .width(200) 和 .flexBasis(80),最终宽度是 80vp 而非 200vp。调试时注意检查。
陷阱 3:百分比基准的计算基准
flexBasis('30%') 的 30% 是相对于容器的可用空间(扣除 padding 后),而不是容器总宽度。如果一个容器 width(360) + padding(16),实际用于计算的宽度是 344vp。
陷阱 4:flexShrink 的默认值
flexShrink 的默认值是 1,而不是 0。这意味着如果所有子项的 flexBasis 之和超出了容器空间,每个子项都会按比例压缩。如果不希望某个子项被压缩,必须显式设置 .flexShrink(0)。
陷阱 5:auto 与 0 的区别
flexBasis(0):基准尺寸为 0,完全由 flexGrow 决定最终尺寸。flexBasis('auto'):基准尺寸由内容决定,flexGrow 仅影响剩余空间。
前者用于「平均分配」,后者用于「内容自适应」。两者行为完全不同。
陷阱 6:Flex 容器本身不设置尺寸时的行为
如果您没有给 Flex 容器显式设置 width 或 height,它的尺寸将由父容器或子组件撑开。此时 flexBasis 的计算基准可能是动态变化的,这可能导致不可预期的布局结果。最佳实践是:始终为 Flex 容器设置明确的尺寸约束(如 .width('100%')),或者使用 .constraintSize() 来设定最小/最大范围。
陷阱 7:flexBasis 与 padding 的叠加效应
flexBasis 设置的是子组件内容区(content box)的主轴尺寸,不包括 padding。如果您在设置了 flexBasis 的子组件上又添加了 padding,子组件在屏幕上实际占用的空间将是 flexBasis + padding。这在视觉上可能让人误以为 flexBasis 没有生效。解决办法是在设置 flexBasis 时,将 padding 也考虑进去。
陷阱 8:嵌套 Flex 时的基准传递
当 Flex 容器中嵌套了另一个 Flex 容器时,外层 Flex 的 flexBasis 作用于内层 Flex 容器整体,而内层 Flex 内部的子项又各自有自己的 flexBasis。这两层弹性计算是独立的,但最终视觉效果是叠加的。调试嵌套 Flex 布局时,建议从外向内逐层分析,先确认外层布局是否正确,再检查内层。
七、完整示例代码
以下是完整的 Index.ets 文件代码,包含了本文讨论的所有演示场景。该代码已在 HarmonyOS NEXT 6.1.0(API 23)上编译通过。
/**
* Flex + flexBasis 基准尺寸布局示例
* ==============================
* 布局要点(鸿蒙原生 ArkTS 布局方式):
*
* 1. Flex 容器:通过 Flex() 构造,默认主轴方向为 Horizontal(水平),
* 可通过 direction 参数设为 FlexDirection.Column 切换为垂直方向。
*
* 2. flexBasis:弹性子组件在「分配弹性空间之前」的初始主轴尺寸。
* - 在 Row 方向布局中,flexBasis 替代了 width 的作用。
* - 在 Column 方向布局中,flexBasis 替代了 height 的作用。
* - 支持值类型:数字(vp)、字符串百分比('30%')、'auto'(内容自适应)。
*
* 3. 弹性分配规则:
* - 先按 flexBasis 为每个子项分配初始尺寸。
* - 剩余空间由 flexGrow(正数)按比例分配。
* - 空间不足时由 flexShrink(正数)按比例压缩。
* - flexGrow 默认为 0,子项不会主动拉伸。
*
* 4. 典型场景(见下方演示):
* ① 等分布局 — 所有子项 flexBasis 相同,主轴均匀分割。
* ② 混合固定尺寸 — 各子项 flexBasis 不同,构成固定比例。
* ③ 百分比基准 — flexBasis 用百分比,相对容器可用空间计算。
* ④ flexBasis + flexGrow — 固定基准 + 剩余空间弹性吸收。
* ⑤ 垂直方向 — Column 下 flexBasis 作用在高度上。
* ⑥ auto 自适应 — flexBasis('auto') 保持内容固有尺寸。
*/
/* ============================================================
* 辅助组件 ① — 演示区标题
* ============================================================ */
@Component
struct DemoTitle {
public title: string = '';
public subTitle: string = '';
build() {
Column() {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1a1a2e')
.width('100%')
.textAlign(TextAlign.Start)
if (this.subTitle) {
Text(this.subTitle)
.fontSize(13)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.Start)
.padding({ bottom: 10 })
}
}
/* ============================================================
* 辅助组件 ② — 带标签的色块
* ============================================================ */
@Component
struct ColorBlock {
public label: string = '';
public color: string = '#409EFF';
public basis: string = '';
build() {
Column() {
Text(this.label)
.fontSize(15)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
if (this.basis) {
Text(this.basis)
.fontSize(11)
.fontColor('rgba(255, 255, 255, 0.9)')
.margin({ top: 2 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(this.color)
.borderRadius(8)
}
}
/* ============================================================
* 辅助组件 ③ — 色块包装器(带右边距)
* ============================================================ */
@Component
struct SpacedBlock {
public label: string = '';
public color: string = '#409EFF';
public basis: string = '';
public isLast: boolean = false;
build() {
ColorBlock({
label: this.label,
color: this.color,
basis: this.basis
})
.margin({ right: this.isLast ? 0 : 8 })
}
}
/* ============================================================
* 主页面
* ============================================================ */
@Entry
@Component
struct FlexBasisDemo {
@State growValue: number = 1;
build() {
Scroll() {
Column() {
/* -------- 页面标题 -------- */
Text('Flex + flexBasis 基准尺寸布局')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#0a0a23')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 24, bottom: 4 })
Text('弹性容器中,flexBasis 决定子组件的初始主轴尺寸')
.fontSize(13)
.fontColor('#888888')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ bottom: 24 })
/* ----- 演示区 ①:等分基准 ----- */
DemoTitle({
title: '① 等分基准 — 所有子项 flexBasis: 80',
subTitle: '容器宽 360vp,三子项各 flexBasis(80),弹性空间均分'
})
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '80vp', isLast: true })
.flexBasis(80)
}
.width(360)
.height(60)
.padding(4)
.backgroundColor('#f0f2f5')
.borderRadius(8)
.margin({ bottom: 28 })
/* ----- 演示区 ②:混合基准 ----- */
DemoTitle({
title: '② 混合基准 — 不同 flexBasis 固定值',
subTitle: 'A=100vp, B=80vp, C=120vp(flexGrow=0 不拉伸)'
})
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '100vp' })
.flexBasis(100)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '120vp', isLast: true })
.flexBasis(120)
}
.width(360)
.height(60)
.padding(4)
.backgroundColor('#f0f2f5')
.borderRadius(8)
.margin({ bottom: 28 })
/* ----- 演示区 ③:百分比基准 ----- */
DemoTitle({
title: '③ 百分比基准 — flexBasis 用百分比值',
subTitle: 'A=30%, B=40%, C=30%(百分比相对容器可用空间计算)'
})
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '30%' })
.flexBasis('30%')
SpacedBlock({ label: 'B', color: '#67C23A', basis: '40%' })
.flexBasis('40%')
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '30%', isLast: true })
.flexBasis('30%')
}
.width(360)
.height(60)
.padding(4)
.backgroundColor('#f0f2f5')
.borderRadius(8)
.margin({ bottom: 28 })
/* ----- 演示区 ④:flexBasis + flexGrow 弹性分配 ----- */
DemoTitle({
title: '④ flexBasis + flexGrow — 基准 + 弹性增长',
subTitle: 'A/B 固定 80vp,C 固定 80vp + flexGrow 吸收剩余空间'
})
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
SpacedBlock({ label: 'A', color: '#409EFF', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'B', color: '#67C23A', basis: '80vp' })
.flexBasis(80)
SpacedBlock({ label: 'C', color: '#E6A23C', basis: '80vp', isLast: true })
.flexBasis(80)
.flexGrow(this.growValue)
.flexShrink(0)
}
.width(360)
.height(60)
.padding(4)
.backgroundColor('#f0f2f5')
.borderRadius(8)
.margin({ bottom: 10 })
/* flexGrow 滑块 */
Row() {
Text('C 的 flexGrow:').fontSize(13).fontColor('#666666')
Slider({ value: this.growValue, min: 0, max: 5, step: 1 })
.width(180)
.onChange((val: number) => { this.growValue = val; })
.blockColor('#E6A23C')
.trackColor('#e0e0e0')
.selectedColor('#E6A23C')
Text(`${this.growValue}`)
.fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#E6A23C').width(28).textAlign(TextAlign.Center)
}
.width('100%').justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 28 })
/* ----- 演示区 ⑤:垂直方向 ----- */
DemoTitle({
title: '⑤ 垂直方向(Column)— flexBasis 作用于高度',
subTitle: 'Flex direction=Column,各子项高度由 flexBasis 决定'
})
Flex({ direction: FlexDirection.Column, wrap: FlexWrap.NoWrap }) {
ColorBlock({ label: 'A', color: '#409EFF', basis: '60vp' })
.flexBasis(60).margin({ bottom: 8 })
ColorBlock({ label: 'B', color: '#67C23A', basis: '40vp' })
.flexBasis(40).margin({ bottom: 8 })
ColorBlock({ label: 'C', color: '#E6A23C', basis: '80vp' })
.flexBasis(80).flexGrow(1)
}
.width(180).height(280).padding(6)
.backgroundColor('#f0f2f5').borderRadius(8)
.margin({ bottom: 28 })
/* ----- 演示区 ⑥:auto 自适应 ----- */
DemoTitle({
title: '⑥ auto 自适应 — 内容决定基准尺寸',
subTitle: 'flexBasis(\'auto\') 保持内容的固有宽度,不强制拉伸'
})
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap }) {
Text('A:短文本').fontSize(14).fontColor(Color.White)
.backgroundColor('#409EFF').borderRadius(8).padding(8)
.flexBasis('auto')
Text('B:这是一段较长的文本内容,我的宽度由内容决定')
.fontSize(14).fontColor(Color.White)
.backgroundColor('#67C23A').borderRadius(8).padding(8)
.margin({ left: 8 }).flexBasis('auto')
Text('C(弹性)').fontSize(14).fontColor(Color.White)
.backgroundColor('#E6A23C').borderRadius(8).padding(8)
.margin({ left: 8 }).flexBasis('auto').flexGrow(1)
}
.width(360).padding(8)
.backgroundColor('#f0f2f5').borderRadius(8)
.margin({ bottom: 32 })
/* ----- 布局要点总结 ----- */
Column() {
Text('📌 flexBasis 布局核心要点').fontSize(17)
.fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
.width('100%').padding({ bottom: 10 })
Text('① flexBasis 在弹性容器(Flex)中取代了主轴方向的固定尺寸:' +
'Row 下取代 width,Column 下取代 height。')
.fontSize(13).fontColor('#333333')
.lineHeight(20).width('100%').padding({ bottom: 6 })
Text('② 分配顺序:所有弹性子项先按 flexBasis 占据初始尺寸,' +
'剩余(或不足)的空间再由 flexGrow / flexShrink 按比例分配。')
.fontSize(13).fontColor('#333333')
.lineHeight(20).width('100%').padding({ bottom: 6 })
Text('③ 支持的值类型:' +
'数字(如 80,单位 vp)| 字符串百分比(如 "30%")| ' +
'字符串 "auto"(由内容决定基准尺寸)')
.fontSize(13).fontColor('#333333')
.lineHeight(20).width('100%').padding({ bottom: 6 })
Text('④ 典型应用:固定侧边栏 + 自适应内容区、' +
'导航栏等分按钮、表格列宽分配、卡片栅格布局。')
.fontSize(13).fontColor('#333333')
.lineHeight(20).width('100%').padding({ bottom: 6 })
Text('⑤ 搭配 flexGrow 可让特定子项吸收剩余空间;' +
'搭配 flexShrink(0) 可防止基准被压缩。' +
'两者共同实现灵活的弹性布局。')
.fontSize(13).fontColor('#333333')
.lineHeight(20).width('100%')
}
.width('100%').padding(16)
.backgroundColor('#f8f9fa').borderRadius(12)
.border({ width: 1, color: '#e8e8e8' })
.margin({ bottom: 40 })
}
.width('100%').padding({ left: 16, right: 16, bottom: 24 })
}
.width('100%').height('100%').backgroundColor(Color.White)
}
}
九、总结
flexBasis 是鸿蒙 ArkTS Flex 布局中控制子组件初始主轴尺寸的核心属性。它与 flexGrow、flexShrink 构成了一个完整的三维弹性模型,能够覆盖绝大多数布局场景。
通过本文的六个演示场景,我们系统地学习了:
- 等分基准:所有子项相同 flexBasis,呈现均匀分布。
- 混合基准:不同 flexBasis 值,实现固定比例布局。
- 百分比基准:使用百分比值,响应容器尺寸变化。
- flexBasis + flexGrow:固定基准 + 弹性增长,实现自适应布局。
- 垂直方向 Column:flexBasis 在垂直方向的应用。
- auto 自适应:内容决定基准尺寸,适用于文本内容不确定的场景。
回顾全文的核心要点,可以用三句话来概括:
第一,flexBasis 是弹性计算的起点——它不是最终尺寸,而是分配空间的基准。理解「初始分配 + 剩余空间再分配」的两步计算模型,是掌握 Flex 布局的关键。
第二,flexBasis、flexGrow、flexShrink 三者必须配合使用——单独使用 flexBasis 只能实现固定尺寸布局,只有结合 flexGrow(拉伸)和 flexShrink(压缩),才能真正发挥 Flex 布局的弹性优势。
第三,动手验证是最好的学习方法——本文配套的示例代码包含了交互式滑块(演示场景④),您可以通过实际操作改变参数值,直观地观察布局变化。强烈建议在 DevEco Studio 中运行示例,边看边学,加深理解。
在实际的鸿蒙应用开发中,Flex + flexBasis 布局方式广泛应用于导航栏、列表项、表单、卡片式布局等场景。掌握它,您将能够轻松应对绝大多数界面布局需求,编写出既美观又响应式的鸿蒙原生应用。
核心要诀: 先按 flexBasis 分配初始空间,再将剩余(或超出)的空间由 flexGrow(或 flexShrink)按比例处理。掌握这个计算模型,就能轻松驾驭 ArkTS 中的 Flex 弹性布局。
本文配套示例代码位于 entry/src/main/ets/pages/Index.ets,已通过 hvigor assembleApp 编译验证。
运行环境:HarmonyOS NEXT 6.1.0(API 23)+ DevEco Studio 6.0+
更多推荐




所有评论(0)