本文参与「「共创季·征文稿事节」第1期开启!写鸿蒙文章,赢AI音箱/登山杖/登山包等大礼包+流量券+Token!」征文活动
开发环境:HarmonyOS NEXT (API 23+) | DevEco Studio 5.0+
项目源码:GitHub — harmonyOSZhengWen


在这里插入图片描述
首页展示

一、写在前面

1.1 鸿蒙生态新篇章

2025 年,HarmonyOS NEXT 正式迈入独立生态的新纪元。这个去掉了 Android 兼容层、完全拥抱鸿蒙内核的操作系统,带来了全新的开发范式和用户体验。对于开发者而言,这意味着真正的「纯血鸿蒙」时代已经到来——不再有 Java 和 Kotlin 的遗留包袱,不再有跨平台桥接的性能损耗,一切从零开始,用鸿蒙原生的方式构建应用。

而在鸿蒙原生应用开发中,ArkUI 声明式 UI 框架是最核心的技术栈。它采用类似 SwiftUI 和 Flutter 的声明式语法,通过 TypeScript 的超集 ArkTS 语言来描述界面,让 UI 代码更简洁、更可预测。

1.2 布局 —— 鸿蒙开发的基石

每个开发者接触一个新 UI 框架时,首先要面对的都是布局问题。布局是构建用户界面的第一道门槛:你需要在屏幕上精确放置每一个元素,控制它们之间的间距、对齐方式和排列顺序。

在 ArkUI 中,布局主要依赖以下几个核心容器:

容器 主轴方向 对应 Web Flexbox 典型应用
Column 垂直(纵向) flex-direction: column 列表、表单、页面骨架
Row 水平(横向) flex-direction: row 导航栏、按钮组、标签栏
Flex 可自定义 display: flex 复杂弹性布局
Stack 层叠 position: absolute 叠加层、悬浮按钮

在这些容器中,Column 是最常用、最基础的一个。从简单的文字排列到复杂的页面骨架,Column 几乎无处不在。而 Column 的 justifyContent 属性,则是控制子组件在垂直方向上分布方式的关键。

1.3 三个让人困惑的 FlexAlign 值

很多刚刚接触 ArkUI 的开发者,在看到 FlexAlign.SpaceBetweenFlexAlign.SpaceAroundFlexAlign.SpaceEvenly 这三个枚举值时,常常一头雾水:

  • “SpaceBetween 和 SpaceAround 有什么区别?”
  • “SpaceEvenly 和 SpaceAround 不是一样的吗?”
  • “我该在什么时候用哪一个?”

这三个模式看起来相似,但实际上在间距计算上有本质区别。如果选错了,布局效果会完全不同——该贴边的没贴边,该留白的没留白。

为了彻底帮大家搞清楚这个问题,我编写了一个完整的 HarmonyOS NEXT 交互式演示应用,用可视化的方式展示每种模式的真实行为。本文将从源码层面深度解析这个应用,逐行讲解关键实现,并分享实战中的最佳实践和常见坑点。


二、项目全景

2.1 应用介绍

「Column 布局方式演示」是一个纯鸿蒙原生的交互式教育应用,它通过三个独立的演示页面,分别展示 SpaceBetweenSpaceAroundSpaceEvenly 三种主轴分布模式的行为特征。

2.2 项目结构

harmonyOSZhengWen/
├── AppScope/
│   └── app.json5                     # 应用全局配置
├── entry/
│   ├── src/main/ets/
│   │   ├── entryability/
│   │   │   └── EntryAbility.ets      # 应用入口 Ability
│   │   ├── entrybackupability/
│   │   │   └── EntryBackupAbility.ets # 备份恢复能力
│   │   └── pages/
│   │       ├── Index.ets             # 导航首页
│   │       ├── ColumnSpaceBetween.ets # SpaceBetween 布局演示
│   │       ├── ColumnSpaceAround.ets  # SpaceAround 布局演示
│   │       └── ColumnSpaceEvenly.ets  # SpaceEvenly 布局演示
│   ├── src/main/resources/
│   │   ├── base/element/
│   │   │   ├── color.json            # 基础颜色资源
│   │   │   ├── float.json            # 浮点数值资源
│   │   │   └── string.json           # 字符串资源
│   │   ├── base/profile/
│   │   │   └── main_pages.json       # 页面路由注册
│   │   └── dark/element/
│   │       └── color.json            # 深色模式颜色覆盖
│   ├── src/main/module.json5         # 模块配置
│   ├── build-profile.json5           # 构建配置
│   └── oh-package.json5              # 包管理配置
├── hvigor/
│   └── hvigor-config.json5           # 构建工具配置
├── build-profile.json5               # 顶层构建配置
├── hvigorfile.ts                     # 构建脚本入口
├── oh-package.json5                  # 顶层包管理
└── local.properties                  # 本地开发环境配置

2.3 技术栈

  • UI 框架:ArkUI(声明式 UI,HarmonyOS NEXT 原生)
  • 开发语言:ArkTS(TypeScript 的超集,强类型)
  • 最低 API 版本:API 23(HarmonyOS NEXT)
  • 页面路由@kit.ArkUIrouter 模块
  • 状态管理@State 装饰器
  • 动画能力.transition() + .animation() API
  • 第三方依赖:零依赖,全部为 ArkUI 内置 API

2.4 功能清单

功能模块 说明
优雅导航首页 三张卡片式导航入口,渐变色装饰,点击跳转
SpaceBetween 演示 首尾贴边分布,2 个中间块可动态显隐
SpaceAround 演示 环绕分布,2 个中间块 + 高度模式切换
SpaceEvenly 演示 全等间距分布,3 个中间块 + 高度模式切换
动态控制按钮 点击显隐中间块,实时观察布局重排
平滑动画过渡 块出现/消失淡入淡出,布局变化过渡平滑
三种布局对比 每个页面底部都有完整的对比表格
返回导航 每个演示页面均可返回首页
深色模式兼容 通过资源限定词适配深色/亮色模式

三、ArkUI 布局体系深入理解

3.1 声明式 UI 的布局哲学

在深入分析具体代码之前,有必要先理解 ArkUI 声明式 UI 的布局哲学。与传统命令式 UI(如 Android View System、Qt Widgets)不同,声明式 UI 不再依赖开发者手动计算布局位置、调用 setX() / setY() 等方法,而是通过声明布局规则,让框架自行计算最终的渲染结果。

// 声明式 UI:你告诉框架「布局长什么样」
Column() {
  Text('顶部')
  Text('中部')
  Text('底部')
}
.justifyContent(FlexAlign.SpaceBetween)
.width('100%')
.height(300)

在上面这段代码中,你声明了一个纵向容器,里面有三个文本,主轴使用 SpaceBetween 分布。剩下的工作——计算每个文本的精确位置、间距大小——全部由 ArkUI 框架自动完成。这就是声明式 UI 的核心优势:关注「是什么」而不是「怎么做」

3.2 主轴与交叉轴

在 ArkUI 中,每个 Flex 容器(Column、Row、Flex)都有两个轴:

  • 主轴(Main Axis):子组件排列的方向。Column 的主轴是垂直方向(从上到下),Row 的主轴是水平方向(从左到右)。
  • 交叉轴(Cross Axis):与主轴垂直的方向。Column 的交叉轴是水平方向,Row 的交叉轴是垂直方向。

justifyContent 控制的是主轴上的对齐方式,而 alignItems 控制的是交叉轴上的对齐方式。

      ←────── 交叉轴 ──────→
  ┌──────────────────────────┐
  │  子组件 A                 │ ↑
  │                          │ │ 主
  │  子组件 B                 │ │ 轴
  │                          │ │
  │  子组件 C                 │ ↓
  └──────────────────────────┘

理解主轴和交叉轴的概念,是正确使用 flex 布局的基础。

3.3 FlexAlign 枚举全解析

ArkUI 的 FlexAlign 枚举定义了 6 种主轴对齐模式。其中前三种是基础对齐,后三种是空间分布模式:

基础对齐模式:

枚举值 效果 空间计算公式
Start 所有子组件从主轴起点开始排列 所有子组件在主轴起点端依次排列,剩余空间全部在终点端
Center 所有子组件在主轴居中排列 子组件整体居中,剩余空间平分到两端
End 所有子组件从主轴终点开始排列 所有子组件在主轴终点端依次排列,剩余空间全部在起点端

空间分布模式(本文重点):

枚举值 效果 空间计算公式
SpaceBetween 首尾贴边,中间均匀分布 有 n 个子组件 → n-1 个间隙,gap = (H - Σh) / (n-1)
SpaceAround 每项两侧环绕空间相等 有 n 个子组件 → n 个间隙单位,gap = (H - Σh) / n,两端各 gap/2
SpaceEvenly 所有空隙(含两端)完全相等 有 n 个子组件 → n+1 个间隙,gap = (H - Σh) / (n+1)

其中 H 为容器主轴方向的尺寸(Column 为高度),Σh 为所有子组件尺寸之和。

3.4 数学模型的直观理解

为了帮助记忆这三种分布模式,可以用一个简单的类比:

SpaceBetween 就像在两根固定柱子之间拉一根弹性绳:

  • 绳子两端绑在柱子上(首尾贴边)
  • 绳子上挂的物品均匀分布(中间均匀分布)
  • 柱子到第一个物品之间没有空隙(两端不留白)

SpaceAround 就像在一个包装盒里放物品,每个物品四周都有泡沫垫:

  • 第一个物品到盒子顶部的泡沫垫 = 中间泡沫垫厚度的一半
  • 每两个物品之间的泡沫垫厚度一致
  • 最后一个物品到盒子底部的泡沫垫 = 中间泡沫垫厚度的一半

SpaceEvenly 就像用切割机把盒子里的空间均匀切割:

  • 顶部空隙 = 物品间空隙 = 底部空隙
  • 一切都是等距的,数学上最完美

四、导航首页 —— 精美入口设计

4.1 设计目标

导航首页(Index.ets)的设计目标是:第一眼就让用户感受到精致和专业。作为应用的"门面",首页不仅要有良好的功能导航能力,还需要在视觉上给人留下深刻印象。

4.2 整体布局结构

首页的布局分为三个区域:

在这里插入图片描述

4.3 数据驱动设计详解

导航卡片使用数据驱动的方式生成,这体现了 ArkUI 声明式 UI 的核心思想——UI 是数据的函数。

首先定义数据模型:

interface NavItem {
  title: string;           // 布局模式名称
  subtitle: string;        // 一行精炼描述
  features: string[];      // 特点列表(用于卡片中的标签)
  gradientStart: string;   // 卡片渐变色起点
  gradientEnd: string;     // 卡片渐变色终点
  accentColor: string;     // 主题色(用于强调文字和按钮)
  icon: string;            // Emoji 图标
  route: string;           // 页面路由路径
}

然后初始化三张卡片的数据:

private readonly navItems: NavItem[] = [
  {
    title: 'SpaceBetween',
    subtitle: '两端对齐 · 首尾贴边',
    features: ['首项紧贴容器顶部', '末项紧贴容器底部', '中间项均匀分布'],
    gradientStart: '#4F46E5',
    gradientEnd: '#7C3AED',
    accentColor: '#4F46E5',
    icon: '↕️',
    route: 'pages/ColumnSpaceBetween'
  },
  {
    title: 'SpaceAround',
    subtitle: '环绕分布 · 两端半间距',
    features: ['子项两侧环绕空间相等', '两端留白 = 中间间距 / 2', '视觉对称,呼吸感强'],
    gradientStart: '#059669',
    gradientEnd: '#10B981',
    accentColor: '#059669',
    icon: '🔄',
    route: 'pages/ColumnSpaceAround'
  },
  {
    title: 'SpaceEvenly',
    subtitle: '全等间距 · 完全对称',
    features: ['所有子项间间距完全相等', '两端留白 = 中间间距', '最均匀的等距分布'],
    gradientStart: '#6366F1',
    gradientEnd: '#8B5CF6',
    accentColor: '#6366F1',
    icon: '⚖️',
    route: 'pages/ColumnSpaceEvenly'
  }
];

最后通过 ForEach 循环渲染所有卡片:

ForEach(this.navItems, (item: NavItem) => {
  this.NavCard(item)
})

这种设计模式的精妙之处在于:数据和 UI 完全分离。如果将来需要增加新的布局模式(比如 Row 的三种分布),只需在数组中新增一项,无需改动任何 UI 代码。

4.4 卡片组件的视觉细节

每张卡片使用了多种视觉技术来提升品质感:

(1)渐变色装饰

.linearGradient({
  direction: GradientDirection.Left,
  colors: [
    [item.gradientStart + '08', 0],   // 3% 透明度的主题色起始
    [item.gradientEnd + '00', 0.95],  // 接近完全透明
    ['#FFFFFF', 1]                     // 纯白结尾
  ]
})

这行代码在卡片上覆盖了一层从主题色到透明的渐变,营造出「左侧有彩色装饰条」的视觉效果。+ '08' 是十六进制颜色 alpha 通道的简洁用法,08 表示 8/255 ≈ 3% 透明度。

(2)圆角阴影

.backgroundColor(Color.White)
.borderRadius(20)
.shadow({
  radius: 12,
  color: '#1A000000',   // 10% 透明度黑色阴影
  offsetX: 0,
  offsetY: 4
})

borderRadius(20) 配合纯白背景,营造出圆角卡片的现代感。shadow() 则让卡片在背景上浮起,形成层次感。

(3)右侧圆形入口按钮

Column() {
  Text('→')
    .fontSize(22)
    .fontColor(item.accentColor)
    .fontWeight(FontWeight.Bold)
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(item.accentColor + '15')  // 8% 透明度主题色背景
.borderRadius(22)

44×44 的圆形按钮,背景色为 8% 透明度的主题色,配合右箭头文字,形成一个优雅的视觉入口。

4.5 首页完整源码分析

以下是 Index.ets 的完整 build() 方法结构:

build() {
  Column() {
    // ═══════════════ ① 页面头部 ═══════════════
    Column() {
      // 渐变色装饰条(Row()
      //   .linearGradient(...))
      // 应用图标(Text('📐'))
      // 主标题(Text('Column 布局方式演示'))
      // 副标题(Text('鸿蒙原生 ArkTS 纵向布局'))
      // 版本标签(Text('HarmonyOS NEXT API 23+'))
    }

    // ═══════════════ ② 导航卡片列表 ═══════════════
    Scroll() {
      Column({ space: 18 }) {
        ForEach(this.navItems, (item: NavItem) => {
          this.NavCard(item)   // 调用 @Builder 方法
        })
      }
      .padding({ left: 20, right: 20, bottom: 20 })
    }
    .layoutWeight(1)   // 占据剩余所有空间

    // ═══════════════ ③ 底部信息 ═══════════════
    Column({ space: 2 }) {
      Text('Column 布局演示应用')
      // ...
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F2F3F8')
}

这里有几个设计细节值得注意:

  • Scroll + layoutWeight(1):让导航卡片区域可以滚动,并占据所有剩余空间。这样在小屏设备上卡片列表可以滚动查看,在平板等大屏设备上则自动撑满。
  • 外层 Column 始终 width('100%').height('100%'):确保根容器撑满全屏,这是所有页面布局的基础。
  • .backgroundColor('#F2F3F8'):浅灰色背景让白色卡片更加突出,形成良好的视觉对比。

五、SpaceBetween:两端对齐 · 首尾贴边

5.1 行为定义

FlexAlign.SpaceBetween 的行为是:第一个子组件紧贴容器顶部,最后一个子组件紧贴容器底部,中间的子组件在剩余空间中均匀分布。容器两端不留空白。

用数学语言描述:假设容器高度为 H,有 n 个子组件,第 i 个子组件的高度为 h_i,则:

  • 第 1 个子组件的顶部 = 0(容器顶部)
  • 第 n 个子组件的底部 = H(容器底部)
  • 第 i 和第 i+1 个子组件之间的间距 = (H - Σh_i) / (n-1)

需要注意的是:当只有 2 个子组件时,SpaceBetween 的效果等价于 Start(首项在顶部,末项在底部,中间项不存在所以没有间距分配)。当只有 1 个子组件时,它会停留在顶部(等同于 Start)。

5.2 适用场景

SpaceBetween 最适合以下场景:

场景一:顶栏 + 内容 + 底栏的典型页面骨架

在大多数应用中,页面结构都是「顶部标题栏 - 中间内容 - 底部操作栏」的经典三段式。使用 SpaceBetween,顶部栏自动贴顶,底部栏自动贴底,中间内容区域自动撑满。

┌──────────────────┐
│  ← 返回    标题    │  ← 贴顶
├──────────────────┤
│                   │
│  主要内容区域       │  ← 自动撑满
│                   │
├──────────────────┤
│  提交    取消      │  ← 贴底
└──────────────────┘

场景二:纵向等分菜单

当需要将几个菜单项在屏幕中均匀分布时,SpaceBetween 可以精确地将它们等间距排列。

场景三:表单的最后操作栏

表单页面底部通常有「提交」和「重置」按钮,使用 SpaceBetween 可以让它们自动分居两端。

5.3 关键源码解析

// ColumnSpaceBetween.ets — 核心布局代码

Column() {
  // 顶部块 —— 不受 if 条件影响,始终显示
  this.DemoBlock('#3B82F6', '🔝 顶部块(紧贴顶部)', 70)

  // 中间块 ① —— 通过 @State 控制显隐
  if (this.showMiddleBlock1) {
    Column() {
      this.DemoBlock('#FF6B6B', '📦 中间块 1(均匀分布)', 70)
    }
    .width('100%')
    .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
    .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
  }

  // 中间块 ② —— 同上
  if (this.showMiddleBlock2) {
    Column() {
      this.DemoBlock('#4ECDC4', '📦 中间块 2(均匀分布)', 70)
    }
    .width('100%')
    .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
    .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
  }

  // 底部块 —— 始终显示
  this.DemoBlock('#845EC2', '⬇ 底部块(紧贴底部)', 70)
}
.width('90%')
// ⭐ 核心设置:仅此一行
.justifyContent(FlexAlign.SpaceBetween)
.height(420)
.padding(16)
.border({ width: 2, color: '#E0E7FF', style: BorderStyle.Dashed })
.borderRadius(16)
.backgroundColor('#F8FAFF')
.animation({ duration: 350, curve: Curve.FastOutSlowIn })

代码逐行解读:

  1. this.DemoBlock(...) 直接放置,不包裹 if 条件:顶部块和底部块始终存在,确保首尾贴边的视觉效果不会消失。这是 SpaceBetween 布局的关键设计——首和尾是"锚点"。

  2. 中间块使用 if 条件包裹:通过 this.showMiddleBlock1this.showMiddleBlock2 两个 @State 变量控制显隐。点击按钮时切换这些变量的值,触发 UI 重渲染。

  3. .transition() 动画:出现时从下方 20vp 淡入,消失时向下方 20vp 淡出。这种垂直方向的过渡动画与 Column 的纵向对齐逻辑协调一致。

  4. .animation() 容器动画:当中间块数量变化时,SpaceBetween 会重新计算间距并调整布局。.animation() 让这个调整过程平滑过渡,而不是瞬间跳跃。

  5. 虚线边框.border({ style: BorderStyle.Dashed }) 在演示区域周围画一个虚线框,直观地标出容器的边界范围,方便用户理解留白和贴边的概念。

5.4 动态交互演示

用户界面包含两个控制按钮,分别控制两个中间块的显示:

Button(this.showMiddleBlock1 ? '隐藏 中间块1' : '显示 中间块1')
  .onClick(() => {
    this.showMiddleBlock1 = !this.showMiddleBlock1;
  })
  .backgroundColor('#FF6B6B')

Button(this.showMiddleBlock2 ? '隐藏 中间块2' : '显示 中间块2')
  .onClick(() => {
    this.showMiddleBlock2 = !this.showMiddleBlock2;
  })
  .backgroundColor('#4ECDC4')

按钮的文案跟随状态变化(「隐藏 中间块1」↔「显示 中间块1」),背景色与对应中间块的颜色一致,形成视觉关联。点击后,中间的块会出现或消失,SpaceBetween 会立即重新计算剩余空间的分配,用户可以看到布局从「3个块」变为「2个块」或「4个块」时的自动调整过程。

这种交互式学习方式比静态截图直观得多——开发者可以亲手操作,亲眼看到布局算法的实时响应。

5.5 图片预览

图·布局演示
在这里插入图片描述
图·隐藏 中间块1
在这里插入图片描述
图·隐藏 中间块2
在这里插入图片描述


六、SpaceAround:环绕分布 · 两端半间距

6.1 行为定义

FlexAlign.SpaceAround 的行为是:每个子组件两侧的空间(环绕间距)相等。子组件之间的间距是子组件到容器边界间距的两倍。

数学表达式:假设容器高度为 H,有 n 个子组件,第 i 个子组件的高度为 h_i,则:

  • 第 1 个子组件到容器顶部的间距 = gap/2
  • 第 n 个子组件到容器底部的间距 = gap/2
  • 子组件之间的间距 = gap
  • 其中 gap = (H - Σh_i) / n

SpaceAround 的名称直译就是「环绕空间的均等分配」——每个子组件周围均匀分布空间,如同行星被轨道环绕。

6.2 与 SpaceBetween 的对比

属性 SpaceBetween SpaceAround
首项到顶部 0(紧贴) gap/2(半间距)
末项到底部 0(紧贴) gap/2(半间距)
中间间距 (H-Σh)/(n-1) (H-Σh)/n
两端视觉 紧凑撑满 留有呼吸空间
典型感觉 紧绷、拉伸 松弛、透气

6.3 适用场景

SpaceAround 在需要视觉"呼吸感"的场景中表现出色:

场景一:功能按钮组

假设有 4 个功能按钮需要在页面底部均匀排列。使用 SpaceBetween 会让第一个按钮紧贴左边缘,最后一个紧贴右边缘(在 Row 容器中),看起来过于拥挤。而 SpaceAround 会在两端留出半间距,让按钮组在视觉上更舒适。

场景二:设置项列表

在设置页面中,每个设置项之间需要留白以便区分。SpaceAround 可以提供均匀的间距,且两端留白让列表看起来更有边界感。

场景三:选项卡片

当多个卡片在容器中纵向排列时,SpaceAround 可以为每张卡片提供等量的上下环绕空间,视觉上更加平衡。

6.4 关键源码解析

SpaceAround 的代码结构与 SpaceBetween 类似,核心区别在 justifyContent 一行。

@Entry
@Component
struct ColumnSpaceAroundDemo {
  @State private showMiddleBlock1: boolean = true;
  @State private showMiddleBlock2: boolean = true;
  @State private useFixedHeight: boolean = true;

  build() {
    Column() {
      // ① 标题区
      Text('Column + SpaceAround 布局演示')
        .fontSize(22).fontWeight(FontWeight.Bold)

      // ② 说明文字
      Text('主轴方向:环绕均等分布,两端留白为中间间距的一半')
        .fontSize(14).fontColor('#8891A3')

      // ③ 核心演示区域
      Column() {
        // 顶部块
        this.DemoBlock('#5B8DEF', '🔝 顶部块', 70)

        // 中间块 ①
        if (this.showMiddleBlock1) {
          Column() {
            this.DemoBlock('#FF6B6B', '📦 中间块 1', 70)
          }
          .width('100%')
          .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
          .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
        }

        // 中间块 ②
        if (this.showMiddleBlock2) {
          Column() {
            this.DemoBlock('#4ECDC4', '📦 中间块 2', 70)
          }
          .width('100%')
          .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
          .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
        }

        // 底部块
        this.DemoBlock('#845EC2', '⬇ 底部块', 70)
      }
      .width('90%')
      // ⭐ 核心设置:SpaceAround
      .justifyContent(FlexAlign.SpaceAround)
      .height(this.useFixedHeight ? 460 : 360)   // 动态切换高度
      // ... 边框、阴影、动画设置

      // ④ 控制按钮区域
      // ...
    }
  }
}

6.5 高度模式切换的独特设计

SpaceAround 页面引入了一个 SpaceBetween 没有的功能——「固定高度 / 自适应高度」切换按钮

Button(this.useFixedHeight ? '切换到「自适应高度」' : '切换到「固定高度」')
  .onClick(() => {
    this.useFixedHeight = !this.useFixedHeight;
  })

useFixedHeight 为 true 时,容器高度为 460vp;为 false 时降为 360vp。这个设计的目的是:

  • 固定高度模式(460vp):容器有充足的空间,SpaceAround 能清晰展示「两端半间距」的特征。用户可以看到顶部块上方和底部块下方有明显的留白,而且这些留白恰好是中间间距的一半。
  • 自适应高度模式(360vp):容器高度缩小,间距被压缩,两端留白几乎消失。这模拟了实际开发中容器尺寸受约束的情况——当空间不足时,SpaceAround 的效果会退化,看起来接近 Start 对齐。

这个对比功能帮助开发者理解:空间分布效果依赖于容器有足够的剩余空间。如果容器高度刚好等于所有子组件高度之和(无剩余空间),那么所有三种模式的效果都一样——子组件紧挨在一起。

6.6 完整的 @Builder DemoBlock

为了复用代码,三个演示页面共享相同模式的 @Builder 方法:

@Builder
DemoBlock(color: string, text: string, height: number) {
  Row() {
    Text(text)
      .fontSize(16)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Medium)
  }
  .width('100%')
  .height(height)
  .backgroundColor(color)
  .borderRadius(12)
  .justifyContent(FlexAlign.Center)
  .shadow({
    radius: 5,
    color: '#33000000',
    offsetX: 0,
    offsetY: 2
  })
}

每个块是一个 Row 容器(横向满宽),内部居中显示文字。设置了背景色、圆角和阴影,让每个块看起来像一张独立的卡片。height 参数由调用方传入,保证每个块在演示区域中占据相同的空间,便于观察间距变化。

6.7 图片预览

图·布局演示
在这里插入图片描述
图·隐藏 中间块1
在这里插入图片描述

图·隐藏 中间块2
在这里插入图片描述

图·切换到“自适应高度”
在这里插入图片描述


七、SpaceEvenly:全等间距 · 完全对称

7.1 行为定义

FlexAlign.SpaceEvenly 的行为是:所有在主轴方向上的空隙——包括首项到容器顶部、子组件之间、末项到容器底部——完全相等。

数学表达式:假设容器高度为 H,有 n 个子组件,第 i 个子组件的高度为 h_i,则:

  • 首项到容器顶部间距 = gap
  • 末项到容器底部间距 = gap
  • 子组件之间的间距 = gap
  • 其中 gap = (H - Σh_i) / (n+1)

SpaceEvenly 是三种模式中数学最对称的一种,它将容器空间均匀切分为 n+1 个等份,子组件位于这些等份之间的分隔线上,而非等份内部。

7.2 适用场景

SpaceEvenly 最适合需要严格对称、视觉平衡的场景。

场景一:导航标签栏

当希望所有导航标签在垂直方向上均匀分布时,SpaceEvenly 可以让标签之间的间距和标签到两端的间距完全一致,视觉效果最为平衡。

场景二:步骤指示器

在注册流程、引导页等多步骤界面中,SpaceEvenly 可以均匀分布每个步骤点,且首尾到边界的留白让步骤不会紧贴屏幕边缘。

场景三:评分或星级显示

当显示 5 颗星的评分时,SpaceEvenly 可以确保星星之间的间距和星星到容器边界的间距一致,视觉均衡。

7.3 关键源码解析

SpaceEvenly 页面是三个演示中最复杂的——它支持 3 个中间块的显隐控制:

@Entry
@Component
struct ColumnSpaceEvenlyDemo {
  @State private showMiddleBlock1: boolean = true;
  @State private showMiddleBlock2: boolean = true;
  @State private showMiddleBlock3: boolean = true;
  @State private useFixedHeight: boolean = true;

  build() {
    Column() {
      // ① 页面标题
      Text('Column + SpaceEvenly 布局演示')
        .fontSize(22).fontWeight(FontWeight.Bold)

      // ② 布局说明
      Text('主轴方向:完全等间距分布(两端间距 = 中间间距)')
        .fontSize(14)

      // ③ 核心演示区域
      Column() {
        // 顶部块
        this.DemoBlock('#6366F1', '🔝 顶部块', 64)

        // 中间块 A(showMiddleBlock1)
        if (this.showMiddleBlock1) {
          Column() {
            this.DemoBlock('#F59E0B', '📦 中间块 A', 64)
          }
          .width('100%')
          .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
          .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
        }

        // 中间块 B(showMiddleBlock2)
        if (this.showMiddleBlock2) {
          Column() {
            this.DemoBlock('#10B981', '📦 中间块 B', 64)
          }
          .width('100%')
          .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
          .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
        }

        // 中间块 C(showMiddleBlock3)
        if (this.showMiddleBlock3) {
          Column() {
            this.DemoBlock('#F43F5E', '📦 中间块 C', 64)
          }
          .width('100%')
          .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: 20 } })
          .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })
        }

        // 底部块
        this.DemoBlock('#8B5CF6', '⬇ 底部块', 64)
      }
      .width('90%')
      // ⭐ 核心设置:SpaceEvenly
      .justifyContent(FlexAlign.SpaceEvenly)
      .height(this.useFixedHeight ? 500 : 400)
      // ... 边框、阴影、动画设置

      // ④ 控制区域(3 个按钮 + 高度切换)
      // ...
    }
  }
}

7.4 三个中间块带来的丰富组合

SpaceEvenly 页面支持 3 个中间块,意味着有 2³ = 8 种组合状态。用户可以通过 3 个独立的按钮来控制每个块的显隐:

Row({ space: 10 }) {
  Button(this.showMiddleBlock1 ? '隐藏 A' : '显示 A')
    .onClick(() => { this.showMiddleBlock1 = !this.showMiddleBlock1; })
    .backgroundColor('#F59E0B')

  Button(this.showMiddleBlock2 ? '隐藏 B' : '显示 B')
    .onClick(() => { this.showMiddleBlock2 = !this.showMiddleBlock2; })
    .backgroundColor('#10B981')

  Button(this.showMiddleBlock3 ? '隐藏 C' : '显示 C')
    .onClick(() => { this.showMiddleBlock3 = !this.showMiddleBlock3; })
    .backgroundColor('#F43F5E')
}

从「全部显示(5 个块)」到「全部隐藏(2 个块)」,用户可以清晰地看到 SpaceEvenly 如何自动重新计算所有间隙(包括两端),使得无论有多少个块,所有空隙始终保持相等。

7.5 图片预览

图·布局演示
在这里插入图片描述
图·隐藏 A
在这里插入图片描述

图·隐藏 B
在这里插入图片描述

图·隐藏 C
在这里插入图片描述

图·切换到“自适应高度”
在这里插入图片描述


八、三种模式的实战对比

8.1 对比表格

每个演示页面底部都附有一个完整的三种布局对比表格(以 SpaceBetween 页面为例):

// 表头
Row() {
  Text('模式').width(100)
  Text('首项贴顶').width(70)
  Text('末项贴底').width(70)
  Text('两端留白').width(70)
}

// SpaceBetween 行(高亮显示)
Row() {
  Text('SpaceBetween').fontColor('#3B82F6').fontWeight(FontWeight.Bold)
  Text('✅').width(70)
  Text('✅').width(70)
  Text('❌').width(70).fontColor('#8891A3')
}
.backgroundColor('#EFF6FF')   // 浅蓝色背景高亮当前模式

// SpaceAround 行
Row() {
  Text('SpaceAround')
  Text('❌').width(70)
  Text('❌').width(70)
  Text('半间距').width(70).fontColor('#059669')
}

// SpaceEvenly 行
Row() {
  Text('SpaceEvenly')
  Text('❌').width(70)
  Text('❌').width(70)
  Text('等间距').width(70).fontColor('#6366F1')
}

表格使用 Row + Text 模拟实现,每行对应一种布局模式。当前正在演示的模式(每个页面对应的模式)会高亮显示——背景色变为浅色调,文字加粗变色。这种设计让用户在操作时能立即定位到表格中的对应行,强化理解。

8.2 综合对比分析

对比维度 SpaceBetween SpaceAround SpaceEvenly
首项位置 紧贴容器顶部 距离顶部 gap/2 距离顶部 gap
末项位置 紧贴容器底部 距离底部 gap/2 距离底部 gap
中间间距大小 (H-Σh)/(n-1) (H-Σh)/n (H-Σh)/(n+1)
两端留白 无(0) 半间距(gap/2) 等间距(gap)
n=2 时的行为 首贴顶,末贴底 首距顶 gap/2,末距底 gap/2 首距顶 gap,末距底 gap
n=1 时的行为 子项在顶部(=Start) 子项居中(=Center) 子项居中(=Center)
视觉特征 紧凑、拉伸、撑满 透气、留白、呼吸感 对称、均衡、规律
适用布局 页眉+内容+页脚 按钮组、卡片列表 导航栏、评分指示器

8.3 选型决策树

在实际项目中,可以用这个决策树快速选择合适的模式:

Q1:容器是否有固定高度或明确的空间约束?
  ├── 否 → 三种模式都可能退化(无剩余空间),优先考虑用 Start/Center
  │         或给容器设置 minHeight 确保有空间分配
  └── 是 → 进入 Q2

Q2:是否希望首项和末项贴边?
  ├── 是 → SpaceBetween
  │        典型场景:页面顶部标题栏 + 底部操作栏
  └── 否 → 进入 Q3

Q3:是否希望两端留白?
  ├── 是,但少量留白(半间距)→ SpaceAround
  │    典型场景:功能按钮组、设置项列表
  └── 是,完全等间距 → SpaceEvenly
       典型场景:导航标签、步骤指示器

8.4 一个具体的实战案例

假设需要实现一个登录页面:

┌──────────────────────┐
│     欢迎回来           │  ← 顶部欢迎语
├──────────────────────┤
│                       │
│  📱 手机号输入框        │
│  🔒 密码输入框          │
│                       │
│  [登录] [注册]         │
│                       │
├──────────────────────┤
│  遇到问题?联系客服      │  ← 底部帮助链接
└──────────────────────┘

分析需求:

  • 顶部欢迎语 → 需要贴顶 → ✅
  • 中部输入框和按钮 → 在中间区域均匀分布
  • 底部帮助链接 → 需要贴底 → ✅

最佳选择: SpaceBetween。顶部紧贴顶部,底部紧贴底部,中间内容在剩余空间中均匀分布。这正是 SpaceBetween 最经典的「三段式布局」用法。

Column() {
  Text('欢迎回来')
    .fontSize(24)
    .fontWeight(FontWeight.Bold)

  // 中间内容区域,在剩余空间中均匀分布
  Column() {
    TextInput({ placeholder: '手机号' })
    TextInput({ placeholder: '密码' })
      .type(InputType.Password)
    Row({ space: 16 }) {
      Button('登录').type(ButtonType.Capsule)
      Button('注册').type(ButtonType.Capsule)
    }
  }
  .layoutWeight(1)
  .justifyContent(FlexAlign.SpaceEvenly)

  Text('遇到问题?联系客服')
    .fontSize(12).fontColor('#999')
}
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)

这个案例展示了两种 justifyContent 的嵌套使用:外层 Column 用 SpaceBetween 实现「首尾贴边」,内层中间区域用 SpaceEvenly 实现「内容均匀分布」。


九、实战经验与常见陷阱

9.1 @Builder 的 void 返回值陷阱

这是本项目中最常遇到的问题,值得重点强调。

问题现象

// ColumnSpaceBetween.ets 第 72 行
if (this.showMiddleBlock1) {
  this.DemoBlock('#FF6B6B', '📦 中间块 1(均匀分布)', 70)
    .transition({ type: TransitionType.Insert, opacity: 0 })  // 编译报错!
}

编译时报错:Property 'transition' does not exist on type 'void'

原因分析

ArkTS 中 @Builder 装饰的方法是一个特殊的语法结构,它的返回值类型被定义为 void。这意味着你不能在 @Builder 调用结果上链式调用组件的方法(如 .transition().width().height() 等)。这不是一个 Bug,而是 ArkTS 语言的有意设计——@Builder 方法生成的是一个构建块(buildable block),而不是一个组件实例。

解决方案

在外层包裹一个容器组件,在容器上调用链式方法:

if (this.showMiddleBlock1) {
  Column() {
    this.DemoBlock('#FF6B6B', '📦 中间块 1(均匀分布)', 70)
  }
  .width('100%')           // ✅ 在 Column 上调用
  .transition({            // ✅ 在 Column 上调用
    type: TransitionType.Insert,
    opacity: 0,
    translate: { y: 20 }
  })
  .transition({
    type: TransitionType.Delete,
    opacity: 0,
    translate: { y: -20 }
  })
}

这个方法的好处是:外层 Column 不仅可以承载 .transition(),还可以设置 .width('100%') 确保子组件撑满宽度,一举两得。

9.2 Color 枚举兼容性问题

问题现象

.backgroundColor(Color.LightGray)  // 编译报错:Property 'LightGray' does not exist

原因分析

HarmonyOS NEXT API 23 的 Color 枚举中,并不是所有直觉上的颜色都存在。经过实际测试,以下颜色在 API 23 中不可用:

不可用枚举 可用的替代方案
Color.LightGray '#D0D0D0'
Color.DarkGray '#555555''#666666'
Color.Gray '#808080'
Color.LightBlue '#E0E7FF'
Color.DarkBlue '#1E3A5F'
Color.LightGreen '#D1FAE5'
Color.DarkGreen '#065F46'
Color.LightRed '#FEE2E2'

最佳实践:统一使用十六进制字符串色值 '#RRGGBB''#AARRGGBB',避免依赖 Color 枚举中可能缺失的成员。字符串色值在所有 API 版本中都兼容,且支持 alpha 通道(通过 8 位色值 '#AARRGGBB' 实现透明度)。

// ✅ 推荐做法:使用字符串色值
.backgroundColor('#D0D0D0')     // LightGray
.backgroundColor('#F0F0F0')     // 更浅的灰色
.backgroundColor('#F8FAFF')     // 非常浅的蓝色调
.backgroundColor('#1A1A2E')     // 深色文字
.backgroundColor('#8891A3')     // 灰色文字
.backgroundColor('#E0E7FF')     // 浅蓝色边框

// 如果需要透明度:使用 8 位色值(前两位为 alpha)
.backgroundColor('#33000000')   // 20% 透明的黑色(阴影颜色)
.backgroundColor('#E0E7FF')     // 完全不透明
.backgroundColor('#1A000000')   // 10% 透明的黑色
.backgroundColor('#08' + '#4F46E5') // 3% 透明度的主题色(拼接方式)

9.3 容器高度与空间分布的关系

一个容易被忽略的点:当容器没有固定高度时,三种分布模式的效果相同

这是因为:

  • Column 的默认高度由其子组件的高度之和决定(wrap_content 行为)
  • 如果容器高度 = 子组件高度之和,则剩余空间 = 0
  • 剩余空间 = 0 时,所有分布模式都没有空间可以分配
  • 子组件之间没有间距,首尾也没有留白
// ❌ 这样的 SpaceEvenly 没有效果(容器高度由内容决定)
Column() {
  Text('A')
  Text('B')
  Text('C')
}
.justifyContent(FlexAlign.SpaceEvenly)
// 没有设置 .height(),也没有设置 .layoutWeight(1)
// 结果:三个文字紧挨在一起,SpaceEvenly 无效

解决方案

// ✅ 方案一:设置固定高度
Column() {
  // ...
}
.justifyContent(FlexAlign.SpaceEvenly)
.height(400)   // 固定高度,框架有空间进行计算

// ✅ 方案二:使用 layoutWeight 撑满父容器
Column() {
  // ...
}
.layoutWeight(1)  // 占据父容器所有剩余空间
.justifyContent(FlexAlign.SpaceBetween)

// ✅ 方案三:使用百分比高度
Column() {
  // ...
}
.height('100%')  // 撑满父容器
.justifyContent(FlexAlign.SpaceAround)

在实际项目中,方案二(layoutWeight)是最推荐的,因为它既不需要固定高度,又能确保容器有足够的空间来执行分布算法。

9.4 深色模式适配

本项目通过资源限定词实现了深色模式适配:

亮色模式颜色resources/base/element/color.json):

{
  "color": [
    { "name": "start_window_background", "value": "#FFFFFF" }
  ]
}

深色模式颜色resources/dark/element/color.json):

{
  "color": [
    { "name": "start_window_background", "value": "#000000" }
  ]
}

HarmonyOS 的资源管理系统会自动根据系统的深色/亮色模式加载对应的资源文件。如果希望代码中也能感知颜色模式,可以通过 ConfigurationConstant.ColorMode 进行判断:

// EntryAbility.ets
import { AbilityConstant, ConfigurationConstant } from '@kit.AbilityKit';

onCreate(want, launchParam) {
  // 设置为跟随系统(默认行为)
  this.context.getApplicationContext()
    .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}

9.5 性能优化建议

虽然这个项目是一个轻量级演示应用,但在实际复杂项目中,以下优化建议值得注意:

1. 避免过度使用动画

.animation().transition() 虽然体验好,但每次状态变化都会触发布局重算和动画帧渲染。对于大量列表项(比如 100+ 个子组件),建议仅在明确需要动画的场景使用这些 API。

2. @State 粒度的控制

@State 变量的粒度应尽可能精细。例如,控制三个中间块显示使用三个独立的 @State 变量 showMiddleBlock1showMiddleBlock2showMiddleBlock3,而不是用一个 number 类型表示状态组合。这样当某个块变化时,只有相关的 UI 部分会重渲染。

3. 合理使用 @Builder

对于复用性高的 UI 片段,使用 @Builder 而不是 @Component 可以减少组件树的深度,提高渲染性能。


十、项目工程化最佳实践

10.1 模块配置

项目的 entry/src/main/module.json5 配置了模块的基本信息:

{
  module: {
    name: 'entry',
    type: 'entry',
    srcEntry: './ets/entryability/EntryAbility.ets',
    description: 'Column布局演示',
    abilities: [
      {
        name: 'EntryAbility',
        srcEntry: './ets/entryability/EntryAbility.ets',
        launchType: 'standard',
        // ...
      }
    ]
  }
}

这里 launchType: 'standard' 表示每次启动都创建新的 Ability 实例。

10.2 页面路由注册

所有页面需要在 main_pages.json 中注册:

{
  "src": [
    "pages/Index",
    "pages/ColumnSpaceBetween",
    "pages/ColumnSpaceAround",
    "pages/ColumnSpaceEvenly"
  ]
}

router.pushUrl({ url: 'pages/ColumnSpaceBetween' }) 会自动匹配这里的注册信息进行页面跳转。

10.3 应用包配置

AppScope/app.json5 定义了应用级别的信息:

{
  app: {
    bundleName: 'com.example.columnlayoutdemo',
    vendor: 'YourCompany',
    version: {
      code: 1,
      name: '1.0.0'
    },
    apiVersion: {
      compatible: 23,
      target: 23
    }
  }
}

apiVersion.compatibleapiVersion.target 分别指定了兼容的最低 API 版本和目标 API 版本。本项目设为 23(HarmonyOS NEXT)。

10.4 .gitignore 配置

项目中应该包含 .gitignore 来屏蔽不需要提交的文件:

# 本地配置
local.properties

# 构建输出
build/
entry/build/

# IDE 配置
.idea/
*.iml

# 包管理器
oh_modules/
node_modules/

十一、总结

11.1 核心收获

通过这个项目的开发和本文的详细解析,我们系统性地学习了鸿蒙 ArkUI 中 Column 容器的三种主轴分布模式:

模式 一句话概括 间距公式 最佳场景
SpaceBetween 首尾贴边,中间均匀 gap = (H-Σh)/(n-1) 三段式页面
SpaceAround 两端半间距,中间等距 gap = (H-Σh)/n 按钮组、卡片列表
SpaceEvenly 所有空隙完全相等 gap = (H-Σh)/(n+1) 导航栏、步骤指示器

11.2 项目质量保障

在开发中,我们坚持了以下原则:

  • 零第三方依赖:所有代码仅使用 ArkUI 内置 API
  • 类型安全:使用 string 色值而非可能缺失的 Color 枚举
  • 充分注释:每个文件含有中英双语注释、ASCII 示意图
  • 交互体验:动画过渡、动态控制、实时反馈
  • API 兼容性:确保所有 API 调用在 API 23 以上版本正常运行

11.3 未来拓展方向

该项目仍有广阔的功能拓展空间:

  1. 增加 Row 容器演示:对比 Column 和 Row 在主轴方向上的差异
  2. 实现 alignSelf 演示:展示单个子组件在交叉轴上的独立对齐
  3. 引入 wrap 换行:当子组件过多时自动换行显示
  4. 嵌套布局演示:展示 Column + Row 复杂嵌套构建真实页面
  5. 主题自定义:让用户自由切换配色方案
  6. 国际化支持:通过 $r 引用字符串资源实现多语言

11.4 致谢与展望

鸿蒙生态正在以前所未有的速度成长。从最初的兼容 Android 到今天的「纯血鸿蒙」,华为用短短几年走完了其他操作系统十几年的路。作为开发者,我们有幸见证并参与这个历史进程。

ArkUI 和 ArkTS 的组合,让鸿蒙原生开发拥有了不亚于 SwiftUI + Swift、Flutter + Dart 的开发体验。声明式 UI 的简洁、强类型语言的安全、以及华为强大的硬件生态,都为鸿蒙原生应用的发展奠定了坚实的基础。

如果你正在学习鸿蒙开发,希望本文的代码示例和实战经验能对你有所帮助。记住:布局是 UI 的骨架,理解 flex 布局的精髓,才能构建出优雅的界面。 在实践中多动手、多尝试、多思考,你会发现 ArkUI 的魅力所在。

最后,再次感谢「鸿蒙原生 ArkTS 布局实战」征文活动提供的分享平台。期待与更多开发者一起交流鸿蒙开发经验,共同推动鸿蒙生态的繁荣发展!


附录 A:完整项目源码速查

A.1 文件清单

文件相对路径 行数 功能
pages/Index.ets ~247 导航首页,三张卡片入口
pages/ColumnSpaceBetween.ets ~271 SpaceBetween 交互式演示
pages/ColumnSpaceAround.ets ~282 SpaceAround 交互式演示
pages/ColumnSpaceEvenly.ets ~288 SpaceEvenly 交互式演示
entryability/EntryAbility.ets ~48 应用入口 Ability
resources/base/element/color.json ~8 基础颜色资源
resources/base/element/string.json ~10 字符串资源
resources/base/profile/main_pages.json ~6 页面路由注册
resources/dark/element/color.json ~8 深色模式颜色

A.2 关键 API 速查表

API 类型 用途 关键参数
.justifyContent(flexAlign) Column / Row 设置主轴对齐方式 FlexAlign.SpaceBetween / SpaceAround / SpaceEvenly
.animation(params) 通用组件 布局变化自动动画 { duration: 350, curve: Curve.FastOutSlowIn }
.transition(params) 通用组件 元素出现/消失动画 { type: TransitionType.Insert, opacity: 0, translate: { y: 20 } }
router.pushUrl(obj) router API 页面路由跳转 { url: 'pages/ColumnSpaceBetween' }
.linearGradient(obj) 通用组件 线性渐变背景 { direction, colors: [color,offset][] }
.shadow(obj) 通用组件 阴影效果 { radius, color, offsetX, offsetY }
.layoutWeight(n) 通用组件 弹性占据剩余空间 正整数
.border(obj) 通用组件 边框 { width, color, style: BorderStyle.Dashed }
@State 装饰器 声明响应式状态变量 变量变化自动触发 UI 重渲染
@Builder 装饰器 声明可复用的构建方法 返回 void,需容器包裹调用链式方法
ForEach(arr, fn) 循环组件 数组遍历渲染 (item, index) => { Component }

本文参与「鸿蒙原生 ArkTS 布局实战」征文活动
开发环境:HarmonyOS NEXT (API 23+) | DevEco Studio 5.0+
项目源码:GitHub — harmonyOSZhengWen
欢迎 Star、Fork、Issue 交流!

Logo

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

更多推荐