鸿蒙原生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 给出了一个优雅的解决方案:

  1. flexBasis 定义「初始」尺寸,而不是「最终」尺寸。
  2. 如果容器有剩余空间,由 flexGrow 决定谁去「吸收」这些空间。
  3. 如果容器空间不足,由 flexShrink 决定谁去「压缩」来腾出空间。

这就是 flexBasis 不可替代的原因——它不只是一个尺寸设定,而是一个弹性计算的起点。

3.2 flexBasis 支持的取值类型

在 ArkTS 中,flexBasis 接受 numberstring 类型的参数:

① 数值(默认单位为 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)

关键变化:directionRow 切换为 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 容器显式设置 widthheight,它的尺寸将由父容器或子组件撑开。此时 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 构成了一个完整的三维弹性模型,能够覆盖绝大多数布局场景。

通过本文的六个演示场景,我们系统地学习了:

  1. 等分基准:所有子项相同 flexBasis,呈现均匀分布。
  2. 混合基准:不同 flexBasis 值,实现固定比例布局。
  3. 百分比基准:使用百分比值,响应容器尺寸变化。
  4. flexBasis + flexGrow:固定基准 + 弹性增长,实现自适应布局。
  5. 垂直方向 Column:flexBasis 在垂直方向的应用。
  6. 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+

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐