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

一、鸿蒙 ArkTS 布局体系与 FlexAlign 概述

1.1 ArkTS 声明式布局体系的演进

HarmonyOS NEXT 6.1.1(API 24)的发布,标志着鸿蒙操作系统正式迈入全栈自研的新阶段。在这个版本中,ArkTS 作为原生应用开发语言,其布局系统得到了进一步的完善和优化。ArkTS 的布局模型继承自 CSS Flexbox 的弹性布局理念,但针对移动端和声明式 UI 范式进行了深度定制和增强。

ArkTS 的布局体系有三大核心设计原则:

第一,声明式描述。 开发者通过函数链式调用来描述布局,而不是通过 XML 或 JSON 配置文件。例如 Row().justifyContent(FlexAlign.SpaceBetween).width('100%') 这种写法直接表达了「水平容器,两端对齐,宽度撑满」的语义。声明式描述的好处是代码即文档,布局意图一目了然。

第二,组件化组合。 ArkTS 将 UI 拆解为可复用的 @Component 组件,通过嵌套组合构建复杂界面。每个组件只负责自己的一亩三分地,降低认知负担。本文示例中的 NavButtonStatCard 就是这种思想的体现。

第三,链式 API。 每个组件的属性设置方法都返回组件实例本身,支持连续调用。这种设计让代码更加紧凑、可读性更强,也让 IDE 的智能提示可以精确到每个方法签名。

1.2 FlexAlign 枚举全景

在 ArkTS 中,RowColumn 容器的 justifyContent 方法接受一个 FlexAlign 枚举值,用于控制子组件在主轴上的分布方式。FlexAlign 定义了 6 种排列策略,可以从两个维度进行归类:

位置型排列(子组件保持自身尺寸,整体在容器中移动):

枚举值 行为描述 视觉特征
FlexAlign.Start 子组件从主轴起始端排列 左对齐/上对齐
FlexAlign.Center 子组件在主轴上居中对齐 居中排列
FlexAlign.End 子组件从主轴尾端排列 右对齐/下对齐

空间分配型排列(子组件之间的间距被弹性分配):

枚举值 行为描述 视觉特征
FlexAlign.SpaceBetween 首尾贴边,中间等间距 两端对齐
FlexAlign.SpaceAround 每个子组件两侧间距相等 环绕均分
FlexAlign.SpaceEvenly 所有间距严格相等 完全等距

本文的重点 —— SpaceBetween —— 属于「空间分配型」排列,它的核心特征是在不改变子组件顺序的前提下,让第一个子组件紧贴容器的起始端,最后一个子组件紧贴容器的终端,剩余的子组件在中间均匀分布,子组件之间的间距完全相同。

1.3 为什么需要两端对齐

在 UI 设计中,两端对齐是最常见的布局需求之一:

  • 顶部导航栏:左侧放返回按钮,中间放标题,右侧放更多操作
  • 底部 Tab 栏:四个或五个 Tab 在底部均匀分布
  • 信息展示栏:「作者 | 日期 | 操作」三栏信息水平排列
  • 标签导航:多个分类标签在容器宽度上均匀分布
  • 表单操作栏:「取消」在左,「确定」在右

如果没有 SpaceBetween,开发者需要手动计算间距、设置 margin,不仅繁琐而且难以适配不同屏幕宽度。SpaceBetween 的出现,让这类布局需求从「手工计算」变为「声明式描述」。


二、两端对齐的核心原理

2.1 主轴与 SpaceBetween 的关系

要理解 SpaceBetween,必须先理解主轴剩余空间这两个概念。

Row 容器中,主轴是水平方向。当子组件在 Row 中排列时,它们从起始端(左)到终端(右)依次排开。子组件占据的总宽度为 Σ(Wi),容器的宽度为 Wcontainer,两者之间的差值就是剩余空间

剩余空间 = Wcontainer - Σ(Wi) - Σ(Padding)

justifyContent(FlexAlign.SpaceBetween) 被设置时,剩余空间会被均匀分配到子组件之间的间隙中:

  • 如果有 N 个子组件,则有 (N-1) 个间隙
  • 每个间隙的宽度 = 剩余空间 / (N-1)
  • 第一个子组件紧贴容器起始端(left padding 之后)
  • 最后一个子组件紧贴容器终端(right padding 之前)

这种分配策略可以用下图表示:

┌─────────────────────────────────────────┐
│← padding →[子1]←──间距──→[子2]←──间距──→[子3]← padding →│
│             ↑        ↑         ↑        ↑               │
│           贴左边  自动计算  自动计算   贴右边             │
└─────────────────────────────────────────┘

2.2 与 layoutWeight 的本质区别

初学者经常混淆 SpaceBetweenlayoutWeight 的效果。它们虽然都能让子组件「展开」,但机制完全不同:

维度 SpaceBetween layoutWeight
控制对象 子组件的位置 子组件的尺寸
分配内容 子组件之间的间隙宽度 子组件自身的宽度增量
子组件尺寸 保持子组件的原始宽度 子组件被拉伸以填满容器
适用场景 固定尺寸子组件的均匀分布 弹性尺寸子组件的比例分配

简单来说:SpaceBetween 管的是「间距」,layoutWeight 管的是「宽度」。

2.3 alignItems 的配合作用

Row 容器中,alignItems 控制的是子组件在交叉轴(垂直方向)的对齐方式。对于 SpaceBetween 布局,通常搭配 alignItems(VerticalAlign.Center) 使子组件在垂直方向居中对齐,视觉上最为协调。

当然,也可以根据设计需求选择其他对齐方式:

  • VerticalAlign.Top:所有子组件顶部对齐,适合图标+文字组合
  • VerticalAlign.Center:所有子组件居中对齐(默认值),通用性最强
  • VerticalAlign.Bottom:所有子组件底部对齐,适合底部操作栏

2.4 数学原理:剩余空间的分配公式

设 Row 容器宽度为 W,左右内边距分别为 PLPR,有 N 个子组件,每个子组件的宽度为 Wi(i 从 1 到 N):

子组件总宽度 Wchildren = Σ(Wi)  (i = 1..N)
可用总宽度 Wavailable = W - PL - PR
剩余空间 Wremaining = Wavailable - Wchildren
间隙数量 GapCount = N - 1
每个间隙宽度 Gap = Wremaining / (N - 1)

第 i 个子组件(从 1 开始编号)的起始位置为:

Position(i) = PL + Σ(Wj) + (i-1) * Gap  (j = 1..i-1)

这个公式在 N ≥ 2 时有效。当 N = 1 时,SpaceBetweenStart 效果相同。这是为什么在演示五中我们特别展示了 2 个、3 个、4 个子组件的情况。


三、SpaceBetween 与其它五种 FlexAlign 的对比分析

3.1 六大取值的行为对比(同一组子组件)

为了直观理解六种 FlexAlign 取值的差异,本文在演示四中使用了完全相同的 4 个子组件(「首」「中1」「中2」「尾」,带不同颜色背景),仅改变 justifyContent 参数。以下是各取值的行为对比:

(1)SpaceBetween

│[首]───────[中1]───────[中2]───────[尾]│
 首贴左                    尾贴右  中间等距

第一个子组件在容器最左侧,最后一个在容器最右侧。中间的间距完全相等。这种排列方式在商业应用中最为常见,因为它既保证了视觉上的对称感,又没有浪费两端的空间。

(2)Start

│[首][中1][中2][尾]                   │
 全部靠左,剩余空间全部在右侧

子组件从容器的起始位置开始排列,所有子组件紧靠左侧,剩余空间全部集中在尾部。这是默认值,也是最简单的排列方式。

(3)Center

│          [首][中1][中2][尾]          │
 全部居中,剩余空间均分到两侧

子组件整体在容器中居中对齐。如果子组件总宽度小于容器宽度,子组件组会居于正中央,两侧剩余空间相等。适用于内容整体居中的场景,比如加载状态提示、居中的对话框按钮组。

(4)End

│                   [首][中1][中2][尾]│
 全部靠右,剩余空间全部在左侧

子组件从容器的尾部开始排列,所有子组件紧靠右侧,剩余空间集中在起始端。适用于操作按钮靠右、语言选择靠右等场景。

(5)SpaceAround

│  [首]────[中1]────[中2]────[尾]  │
 每个子组件两侧间距相等

每个子组件两侧的间距相等。注意两端间距是内部间距的一半(因为两端各一份,而中间有两个间距叠加)。视觉上子组件之间的间距看起来比两端到容器的间距更大。

(6)SpaceEvenly

│   [首]────[中1]────[中2]────[尾]   │
 所有间距严格相等

所有间距(包括子组件与容器边缘的间距)完全相等。这是最均匀的分布方式,视觉效果最为平衡,但代价是子组件组不再紧贴容器边缘。

3.2 视觉密度与信息利用率的对比

从信息利用率的维度来看,六种排列方式存在明显差异:

排列方式 两端空间利用 中心紧凑度 适用信息类型
SpaceBetween ★★★★★ ★★★☆☆ 导航、工具栏
Start ★☆☆☆☆ ★★★★☆ 列表、表单
Center ★★☆☆☆ ★★★★☆ 弹窗、提示
End ★☆☆☆☆ ★★★☆☆ 右对齐操作
SpaceAround ★★★☆☆ ★★★☆☆ 图标栏、评分
SpaceEvenly ★★★★☆ ★★☆☆☆ 底部导航、Tab

3.3 何时选择 SpaceBetween

以下场景优先考虑 SpaceBetween

  1. 至少需要 2 个子组件 —— 只有一个组件时与 Start 无异
  2. 子组件宽度固定或接近固定 —— 如果子组件需要弹性伸缩,考虑 layoutWeight
  3. 需要首尾贴边 —— 如果设计上要求两端留白,考虑 SpaceAroundSpaceEvenly
  4. 子组件数量相对较少 —— 子组件过多时(超过 7~8 个),SpaceBetween 可能导致子组件过于拥挤,此时考虑换行或使用 Scroll

四、完整示例应用架构设计

4.1 项目结构

本示例应用是一个专注于演示 Row + justifyContent(FlexAlign.SpaceBetween) 布局的 ArkTS 页面应用。项目结构如下:

MyApplication/
├── entry/
│   ├── build-profile.json5                           # 构建配置
│   ├── hvigorfile.ts                                # 编译脚本
│   └── src/
│       └── main/
│           └── ets/
│               ├── entryability/
│               │   └── EntryAbility.ets              # 应用入口
│               └── pages/
│                   └── RowSpaceBetweenDemo.ets        # 主页面(本文核心)
├── hvigor/
│   └── hvigor-config.json5
└── build-profile.json5

4.2 应用入口配置

EntryAbility.ets 是应用的 Ability 入口,负责在 onWindowStageCreate 生命周期中加载主页面:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageCreate');
    // ★ 关键:加载 RowSpaceBetween 布局演示页面
    windowStage.loadContent('pages/RowSpaceBetweenDemo', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'EnglishApp',
          'Failed to load content: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
    });
  }

  onWindowStageDestroy(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onBackground');
  }
}

4.3 页面整体布局结构

RowSpaceBetweenPage 页面的 build() 方法采用了一个纵向全屏 Column + 可滚动内容 + 底部说明面板的三段式结构:

Column(全屏背景 #eef2f7)
├── 页面标题区(青色背景 #1a6b5a)
│   ├── 主标题:"📐 Row + justifyContent(SpaceBetween)"
│   └── 副标题:"主轴(水平)两端对齐分布 · 首尾贴边,中间等间距"
├── Scroll 滚动容器
│   └── Column(内部 Column 承载 7 个演示区)
│       ├── ❶ 基础演示:三栏导航标题
│       ├── ❷ 底部导航栏(4 项均分)
│       ├── ❸ 数据统计面板
│       ├── ❹ 六大排列方式横向对比
│       ├── ❺ 子组件数量对 SpaceBetween 的影响
│       ├── ❻ 真实场景:文章底部信息栏
│       └── ❼ 标签导航栏
└── 布局技术说明面板
    ├── 要点列表(6 个 ● 要点)
    ├── 分隔线
    └── 核心代码展示

这种结构设计的优点:

  • 标题区:固定显示,始终告知用户当前演示主题
  • 滚动区:容纳 7 个相对独立的演示,不会因为内容过多而撑满屏幕
  • 说明面板:底部固定,汇总核心知识要点,方便查阅

五、子组件设计与数据模型

5.1 数据模型定义

我们定义了两个接口来承载演示数据:

/**
 * 导航项数据
 */
interface NavItem {
  icon: string;     // 导航图标(emoji)
  label: string;    // 导航标签文字
  isActive: boolean; // 是否选中状态
}

/**
 * 统计卡片数据
 */
interface StatItem {
  label: string;    // 统计项名称
  value: string;    // 统计数值
  color: string;    // 主题色
}

接口设计遵循「最小化」原则——每个字段都在组件中被实际使用,没有冗余。

5.2 导航按钮组件 NavButton

NavButton 是一个图标+文字的组合按钮,用于底部导航栏:

@Component
struct NavButton {
  private item: NavItem = { icon: '', label: '', isActive: false };

  build() {
    Column() {
      Text(this.item.icon)
        .fontSize(24)
        .lineHeight(30)
        .opacity(this.item.isActive ? 1.0 : 0.5)  // 选中态高亮

      Text(this.item.label)
        .fontSize(10)
        .fontColor(this.item.isActive ? '#3a7bd5' : '#888888')
        .fontWeight(this.item.isActive ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 6, bottom: 4 })
  }
}

技术要点:

  • 使用 isActive 控制选中态和未选中态的样式差异——透明度、文字颜色、字重三个维度同时变化,使选中态更加醒目
  • Row 容器中使用 SpaceBetween 时,每个 NavButton 占用其自身的实际宽度,剩余空间自动分配到各导航项之间

5.3 统计卡片组件 StatCard

StatCard 用于展示统计数据,内部使用 Column 使数值在上、标签在下:

@Component
struct StatCard {
  private item: StatItem = { label: '', value: '', color: '' };

  build() {
    Column() {
      Text(this.item.value)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.item.color)
        .lineHeight(36)

      Text(this.item.label)
        .fontSize(12)
        .fontColor('#888888')
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .width(80)          // 固定宽度,方便 SpaceBetween 计算间距
    .padding({ top: 16, bottom: 16 })
    .backgroundColor('#f8f9fc')
    .borderRadius(12)
  }
}

关键设计:StatCard 固定了 width(80),这使得在 Row 容器中使用 SpaceBetween 时,4 个卡片的总宽度为 80 * 4 = 320vp,剩余空间被均匀分配到 3 个间隙中,视觉效果非常整齐。

5.4 主页面数据成员

@Entry
@Component
struct RowSpaceBetweenPage {
  /** 底部导航栏数据(4 项) */
  private readonly navItems: NavItem[] = [
    { icon: '🏠', label: '首页', isActive: true },
    { icon: '🔍', label: '发现', isActive: false },
    { icon: '📱', label: '动态', isActive: false },
    { icon: '👤', label: '我的', isActive: false },
  ];

  /** 统计面板数据(4 项) */
  private readonly statItems: StatItem[] = [
    { label: '粉丝', value: '12.8K', color: '#f5a623' },
    { label: '关注', value: '368', color: '#4a90d9' },
    { label: '获赞', value: '8.6K', color: '#7ed321' },
    { label: '访问', value: '45.2K', color: '#d0021b' },
  ];

  /** 标签导航数据(7 项) */
  private readonly tagLabels: string[] = [
    '全部', '推荐', '鸿蒙', 'ArkTS', '布局', '组件', '实战',
  ];
}

注意所有数据成员都声明为 private readonly,因为它们在整个页面生命周期中不会发生变化。这避免了不必要的响应式数据开销。


六、演示一:三栏导航标题 —— SpaceBetween 入门

6.1 场景描述

「三栏导航标题」是移动应用中极为常见的界面模式:页面顶部有一个导航栏,左侧是返回按钮,中间是页面标题,右侧是更多操作按钮。

在鸿蒙原生应用中,很多新手会这样实现:

Row() {
  Text('← 返回').margin({ right: 'auto' })  // ❌ 错误的思路
  Text('文章详情')
  Text('更多 ···').margin({ left: 'auto' })   // ❌ 错误的思路
}

但 ArkTS 并不支持 margin: auto 的写法。正确的做法是使用 justifyContent(FlexAlign.SpaceBetween)

6.2 实现代码

// ★ 核心:Row + justifyContent(SpaceBetween) ★
Row() {
  Text('← 返回')
    .fontSize(14)
    .fontColor('#333')
    .fontWeight(FontWeight.Medium)

  Text('文章详情')
    .fontSize(16)
    .fontColor('#1a1a2e')
    .fontWeight(FontWeight.Bold)

  Text('更多 ···')
    .fontSize(14)
    .fontColor('#333')
    .fontWeight(FontWeight.Medium)
}
// =============================================
//  核心布局属性:
//  justifyContent(FlexAlign.SpaceBetween)
//  效果:"← 返回" 贴左,"更多 ···" 贴右,"文章详情" 居中
// =============================================
.justifyContent(FlexAlign.SpaceBetween)  // ← ★ 两端对齐 ★
.alignItems(VerticalAlign.Center)
.width('100%')
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor('#ffffff')
.borderRadius(10)
.border({ width: 1, color: '#e8ecf0' })

6.3 运行效果

当应用运行在模拟器或真机上时,导航栏呈现出:

┌─────────────────────────────────────┐
│ ← 返回          文章详情          更多 ··· │
└─────────────────────────────────────┘

三个文本组的排布规律:

  • 「← 返回」紧贴导航栏左侧
  • 「更多 ···」紧贴导航栏右侧
  • 「文章详情」位于两者之间的正中央(因为左右两边的间距相等)

6.4 布局计算过程

假设屏幕宽度为 360vp,导航栏 width('100%') 占满屏幕宽度,padding 左右各 12vp,则有效宽度为:

Wavailable = 360 - 12 - 12 = 336vp

假设「← 返回」宽度 60vp,「文章详情」宽度 80vp,「更多 ···」宽度 70vp,三个子组件总宽度 210vp:

Wremaining = 336 - 210 = 126vp
GapCount = 3 - 1 = 2
Gap = 126 / 2 = 63vp

因此「← 返回」距离左侧边框 12vp,距离「文章详情」63vp,后者距离「更多 ···」63vp,「更多 ···」距离右侧边框 12vp。


七、演示二:底部导航栏 —— 移动端 TabBar 均分

7.1 场景描述

底部导航栏(Bottom Navigation Bar)是 Material Design 定义的经典组件,通常包含 3 到 5 个导航项。在鸿蒙应用中,底部导航栏同样是最常见的布局组件之一。

7.2 实现代码

Row() {
  ForEach(this.navItems, (item: NavItem) => {
    NavButton({ item: item })
  })
}
// =============================================
//  justifyContent(SpaceBetween) 让 4 个 Tab 在底部均匀分布
//  第一个 Tab 贴左侧,最后一个贴右侧,中间均分
// =============================================
.justifyContent(FlexAlign.SpaceBetween)  // ← ★ Tab 栏均分 ★
.alignItems(VerticalAlign.Center)
.width('100%')
.height(56)
.padding({ left: 20, right: 20 })
.backgroundColor('#ffffff')
.borderRadius(12)
.border({ width: 1, color: '#e0e4e8' })

7.3 为什么选择 SpaceBetween 而非 SpaceEvenly

这是很多开发者容易困惑的问题——底部导航栏的 Tab 应该用 SpaceBetween 还是 SpaceEvenly

答案是:绝大多数情况下应该使用 SpaceBetween

原因在于:

  • SpaceBetween 让第一个 Tab 和最后一个 Tab 分别贴边,导航栏的有效区域被充分利用
  • SpaceEvenly 的两端也会留出与内部间隙相同的间距,导致左右两侧出现多余的空白,导航栏看起来不够紧凑
  • 移动端屏幕空间有限,「贴边设计」可以最大化信息密度

用图示对比:

SpaceBetween:
│[首页]──────[发现]──────[动态]──────[我的]│
  ↑ 贴左                           ↑ 贴右

SpaceEvenly:
│   [首页]────[发现]────[动态]────[我的]   │
  ↑ 有间距                        ↑ 有间距

7.4 高度 56vp 的设计依据

演示二中 Row 容器设置了 height(56)。56vp 是移动端底部导航栏的标准高度,这是经过大量可用性测试得出的结果——它既足够容纳图标和标签文字,又不会占用太多屏幕空间,同时满足触摸操作的最小目标尺寸(建议 44vp 以上)。


八、演示三:数据统计面板 —— 多卡片均分布局

8.1 场景描述

个人主页中的统计面板——「粉丝」「关注」「获赞」「访问」四项数据在卡片中横向排列。这是 SpaceBetween 在固定宽度卡片布局中的典型应用。

8.2 实现代码

Row() {
  ForEach(this.statItems, (item: StatItem) => {
    StatCard({ item: item })
  })
}
// =============================================
//  4 个 StatCard 在 Row 中两端对齐均匀分布
//  每个卡片宽度固定 80vp,剩余空间在卡片之间等分
// =============================================
.justifyContent(FlexAlign.SpaceBetween)  // ← ★ 统计卡片均分 ★
.alignItems(VerticalAlign.Center)
.width('100%')
.padding({ left: 4, right: 4 })
.backgroundColor('#f0f4f8')
.borderRadius(12)
.padding(12)

8.3 固定宽度与弹性间距

这里的关键设计是 StatCard 设置了 width(80) 固定宽度。为什么固定宽度如此重要?

因为 SpaceBetween 的间距分配依赖于子组件的实际宽度。如果子组件的宽度不固定(比如由内容撑开),那么在不同的屏幕宽度或不同的数据内容下,间距的计算结果会不同,导致布局不稳定。

固定宽度的优点

  • 间距计算结果可预测,布局稳定
  • 适配不同屏幕时,间距自动等比例变化
  • 每个卡片的尺寸一致,视觉整齐

弹性宽度的适用场景

  • 子组件内容长度变化较大,无法固定
  • 需要子组件等比例瓜分容器空间
  • 内容自适应比整齐排列更重要

8.4 间距的动态计算

假设容器有效宽度为 320vp(扣除左右 padding 后的值),4 个固定宽度为 80vp 的卡片:

子组件总宽度 = 80 × 4 = 320vp
剩余空间 = 320 - 320 = 0vp

当剩余空间为 0 时,SpaceBetween 的效果与 Start 相同——所有子组件贴在一起。但实际上,因为我们在 Row 外部还包了一个 padding(12) 的外层容器(演示三的 Column 容器),加上 Row 本身的 padding({ left: 4, right: 4 }),实际的可用宽度会比 4 个卡片的总宽度大一些,从而产生间距。

这就是为什么在有限宽的容器中,即使设置了 SpaceBetween,也需要配合适当的 padding 才能让间距显现出来。


九、演示四:六大排列方式横向对比大全

9.1 设计意图

演示四是整个示例中信息密度最高的演示区。它使用完全相同的 4 个子组件(「首」「中1」「中2」「尾」,分别用橙、蓝、绿、红四色区分),仅改变 justifyContent 的参数,展示 6 种排列方式的视觉差异。

这种「控制变量法」的设计,让读者可以在同一视口内直观对比,形成深刻的视觉记忆。

9.2 代码结构

出于篇幅考虑,这里展示对比 1(SpaceBetween)和对比 6(SpaceEvenly)的代码,其余 4 种结构相同仅参数不同:

// ── 对比 1:SpaceBetween(两端对齐) ──
Row() {
  Text('首').fontSize(13).fontColor('#fff').padding(8)
    .backgroundColor('#f5a623').borderRadius(6)
  Text('中1').fontSize(13).fontColor('#fff').padding(8)
    .backgroundColor('#4a90d9').borderRadius(6)
  Text('中2').fontSize(13).fontColor('#fff').padding(8)
    .backgroundColor('#7ed321').borderRadius(6)
  Text('尾').fontSize(13).fontColor('#fff').padding(8)
    .backgroundColor('#d0021b').borderRadius(6)
}
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(40)
.padding({ left: 8, right: 8 })
.backgroundColor('#f8f9fc')
.borderRadius(8)
.margin({ bottom: 12 })

// ...(对比 2 Start、对比 3 Center、对比 4 End 省略,仅 justifyContent 参数不同)

// ── 对比 6:SpaceEvenly(完全等距) ──
Row() {
  // 子组件同上...
}
.justifyContent(FlexAlign.SpaceEvenly)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(40)
.padding({ left: 8, right: 8 })
.backgroundColor('#f0f0f0')
.borderRadius(8)

9.3 六种排列的效果对比表

为了帮助读者更直观地理解差异,我们对六种排列的效果进行量化对比:

排列方式 首位置 尾位置 间距特征 两端留白
SpaceBetween 贴左边缘 贴右边缘 内部间距相等
Start 贴左边缘 跟随排列 无额外间距 右侧大量
Center 居中偏左 居中偏右 无额外间距 两侧相等
End 跟随排列 贴右边缘 无额外间距 左侧大量
SpaceAround 距左边缘 Gap/2 距右边缘 Gap/2 内部间距 = Gap 两端 Gap/2
SpaceEvenly 距左边缘 Gap 距右边缘 Gap 内部间距 = Gap 两端 = Gap

其中 Gap = 剩余空间 / (子组件数量 + 1),SpaceAround 的特殊之处在于两端间距是内部间距的一半。

9.4 标签式的视觉引导

为了方便读者快速定位当前查看的是哪种排列方式,每个对比行上方都有一个带颜色的标签文字:

// SpaceBetween — 蓝色标签(高亮)
Text('▶ justifyContent(SpaceBetween) — 两端对齐,中间均分')
  .fontSize(12).fontColor('#3a7bd5').fontWeight(FontWeight.Medium)

// 其他排列 — 灰色标签(非高亮)
Text('▶ justifyContent(Start) — 靠左排列(对比)')
  .fontSize(12).fontColor('#888').fontWeight(FontWeight.Medium)

通过颜色区分「焦点」和「参考」,引导读者的视觉注意力集中在 SpaceBetween 上,其他排列作为对比参照。


十、演示五:子组件数量对 SpaceBetween 的影响

10.1 设计意图

SpaceBetween 的行为与子组件的数量密切相关。很多开发者在使用时会遇到「为什么只有 2 个子组件时间距很大 / 没有间距」的困惑。演示五通过逐步展示 2 个、3 个、4 个子组件的行为,帮助理解 SpaceBetween 的数学原理。

10.2 2 个子组件的实现

Row() {
  Text('左侧').fontSize(13).fontColor('#fff').padding(10)
    .backgroundColor('#4a90d9').borderRadius(6)
  Text('右侧').fontSize(13).fontColor('#fff').padding(10)
    .backgroundColor('#d0021b').borderRadius(6)
}
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor('#f8f9fc')
.borderRadius(8)

当只有 2 个子组件时,SpaceBetween 的效果是:一个在起始端,一个在终端。中间没有其他子组件,所以看起来就像两端对齐。用数学公式表达:

子组件 N = 2, 间隙数量 = 1
剩余空间全部集中在两个子组件之间的唯一间隙中

视觉上:

│[左侧]←──────── 所有剩余空间 ────────→[右侧]│

10.3 3 个子组件的实现

Row() {
  Text('左').fontSize(13).fontColor('#fff').padding(10)
    .backgroundColor('#f5a623').borderRadius(6)
  Text('中').fontSize(13).fontColor('#fff').padding(10)
    .backgroundColor('#4a90d9').borderRadius(6)
  Text('右').fontSize(13).fontColor('#fff').padding(10)
    .backgroundColor('#7ed321').borderRadius(6)
}
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor('#f8f9fc')
.borderRadius(8)

当有 3 个子组件时,第一个贴左、最后一个贴右,中间的组件位于两者之间的正中央。间距数量为 2,剩余空间被平分为两份。

子组件 N = 3, 间隙数量 = 2
Gap1 = Gap2 = Wremaining / 2

视觉上:

│[左]←── Gap1 ──→[中]←── Gap2 ──→[右]│

10.4 4 个子组件的实现

Row() {
  Text('1').fontSize(13).fontColor('#fff').width(40).height(30)
    .backgroundColor('#f5a623').borderRadius(6)
    .textAlign(TextAlign.Center)
  Text('2').fontSize(13).fontColor('#fff').width(40).height(30)
    .backgroundColor('#4a90d9').borderRadius(6)
    .textAlign(TextAlign.Center)
  Text('3').fontSize(13).fontColor('#fff').width(40).height(30)
    .backgroundColor('#7ed321').borderRadius(6)
    .textAlign(TextAlign.Center)
  Text('4').fontSize(13).fontColor('#fff').width(40).height(30)
    .backgroundColor('#d0021b').borderRadius(6)
    .textAlign(TextAlign.Center)
}
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.width('100%')
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor('#f8f9fc')
.borderRadius(8)

4 个子组件时,3 个间隙将剩余空间三等分:

子组件 N = 4, 间隙数量 = 3
Gap1 = Gap2 = Gap3 = Wremaining / 3

视觉上:

│[1]←Gap1→[2]←Gap2→[3]←Gap3→[4]│

10.5 规律总结

从 2 个到 4 个子组件,可以总结出清晰的规律:

子组件数 间隙数 间距均分方式 视觉效果
2 1 剩余空间×1 左右分开
3 2 剩余空间÷2 左中右均分
4 3 剩余空间÷3 四端均分
N N-1 剩余空间÷(N-1) 首尾贴边,中间均分

一般规律:Gap = Wremaining / (N - 1),其中 N ≥ 2。


十一、演示六:真实场景 —— 文章底部信息栏

11.1 场景描述

演示六模拟了一个文章详情页的底部信息栏,这是 SpaceBetween 在实际产品中最常见的应用场景之一。信息栏包含三部分内容:

  • 左侧:作者信息(头像 + 昵称)
  • 中间:发布时间
  • 右侧:互动数据(点赞数 + 评论数)

11.2 实现代码

// ★ 核心:底部信息栏 — Row + SpaceBetween ★
Row() {
  // 左侧:作者信息
  Row() {
    Text('👤').fontSize(18).lineHeight(22)
    Text('鸿蒙开发者')
      .fontSize(12)
      .fontColor('#555')
      .fontWeight(FontWeight.Medium)
      .margin({ left: 6 })
  }
  .alignItems(VerticalAlign.Center)

  // 中间:发布时间
  Text('2026-06-20')
    .fontSize(11)
    .fontColor('#aaa')

  // 右侧:操作按钮组
  Row() {
    Text('❤️ 128').fontSize(12).fontColor('#d0021b').margin({ right: 12 })
    Text('💬 36').fontSize(12).fontColor('#888')
  }
  .alignItems(VerticalAlign.Center)
}
// =============================================
//  核心:SpaceBetween 让「作者 | 日期 | 操作」三栏两端对齐
//  作者信息贴左,操作按钮贴右,日期居中
// =============================================
.justifyContent(FlexAlign.SpaceBetween)  // ← ★ 信息栏两端对齐 ★
.alignItems(VerticalAlign.Center)
.width('100%')
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor('#ffffff')
.borderRadius({ bottomLeft: 12, bottomRight: 12 })

11.3 嵌套 Row 的使用

注意这个演示中出现了嵌套 Row 的使用模式:

外层 Row (SpaceBetween)
├── 内层 Row 1(作者信息,水平排列)
│   ├── Text('👤')
│   └── Text('鸿蒙开发者')
├── Text(发布时间)
└── 内层 Row 2(操作按钮,水平排列)
    ├── Text('❤️ 128')
    └── Text('💬 36')

为什么左侧和右侧要包裹一层 Row?因为这两部分各自包含多个组件(左侧是头像+昵称,右侧是点赞+评论),它们内部需要水平排列。外层 Row 负责「作者 | 时间 | 操作」三栏的 SpaceBetween 整体布局,内层 Row 负责各自内部元素的水平排列。

这种分层嵌套的设计思路是鸿蒙 ArkTS 布局的核心方法论——每个 RowColumn 只负责一个维度的布局,通过组合形成复杂布局。

11.4 真实感的数据填充

为了让演示区看起来更真实,我们在信息栏上方模拟了文章内容区域:

Column() {
  Text('🏔️ 鸿蒙原生开发最佳实践')
    .fontSize(15)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1a1a2e')

  Text('本文详细介绍了 HarmonyOS NEXT 平台下 ArkTS 布局的最佳实践...')
    .fontSize(12)
    .fontColor('#666')
    .lineHeight(20)
    .margin({ top: 6 })

  Text('(内容略,此处仅为演示底部信息栏布局效果)')
    .fontSize(11)
    .fontColor('#bbb')
    .margin({ top: 4 })
}

这种「上下文还原」的设计有助于读者理解 SpaceBetween 在真实界面中的位置和效果,比孤立的代码片段更有说服力。


十二、演示七:标签导航栏 —— 多标签均匀分布

12.1 场景描述

演示七展示了分类标签导航栏 —— 7 个标签(全部、推荐、鸿蒙、ArkTS、布局、组件、实战)在容器宽度上均匀分布。SpaceBetween 使首标签贴左、尾标签贴右、中间标签等间距排列。

12.2 实现代码

Row() {
  ForEach(this.tagLabels, (label: string) => {
    Text(label)
      .fontSize(14)
      .fontColor(label === '全部' ? '#ffffff' : '#555')
      .fontWeight(label === '全部' ? FontWeight.Bold : FontWeight.Normal)
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .backgroundColor(label === '全部' ? '#3a7bd5' : '#f0f0f0')
      .borderRadius(16)
  })
}
// =============================================
//  7 个标签通过 SpaceBetween 在容器中两端对齐
//  首标签贴左,尾标签贴右,中间标签等间距分布
// =============================================
.justifyContent(FlexAlign.SpaceBetween)  // ← ★ 标签均分 ★
.alignItems(VerticalAlign.Center)
.width('100%')
.padding({ left: 4, right: 4 })
.backgroundColor('#ffffff')
.borderRadius(12)

12.3 选中态的视觉区分

在标签组中,第一个标签「全部」被设为选中态,其他标签为未选中态。选中态通过三处视觉变化来区分:

  • 背景色#3a7bd5(蓝色) vs #f0f0f0(浅灰)
  • 文字颜色#ffffff(白色) vs #555555(深灰)
  • 字重FontWeight.Bold(粗体) vs FontWeight.Normal(常规)

这种多维度的视觉区分确保了辅助功能(如色盲用户)也能通过文字粗细和颜色对比度区分选中和非选中状态。

12.4 标签数量较多的思考

当子组件数量增加到 7 个时,SpaceBetween 面临一个实际挑战:如果标签的文本长度差异较大,或者屏幕宽度较小,标签之间可能没有足够的间距。

解决办法有几种思路:

  1. 使用 SpaceAround:让两端也有一定间距,使标签在视觉上更舒展
  2. 使用 Scroll 包裹:当标签总宽度超过容器宽度时,允许水平滚动
  3. 缩小标签内边距:减少 padding 的值,让标签更紧凑
  4. 使用 Wrap 换行:允许标签在容器宽度不足时自动换行(适用于 Flex 容器的 wrap 模式)

在本演示中,7 个标签的文字长度较短(每个 2~4 个字符),配合适当的 padding 值,SpaceBetween 仍能产生清晰可辨的间距效果。


十三、布局技术要点深度总结

13.1 核心规则速记

SpaceBetween,两端紧贴边
中间留间距,自动等分完
至少两个子,否则没效果
Row 里横着走,Column 里竖着站

13.2 六个必须知道的事实

事实一:SpaceBetween 只分配间隙,不改变子组件尺寸。

SpaceBetween 不会拉伸或压缩子组件,子组件保持其原始尺寸。如果需要子组件弹性伸缩,应使用 layoutWeight

事实二:至少需要 2 个子组件才有视觉效果。

当只有 1 个子组件时,SpaceBetweenStart 等效——子组件位于起始端。

事实三:间隙数量始终比子组件数量少 1。

N 个子组件有 N-1 个间隙,剩余空间被 N-1 等分。

事实四:子组件的宽度会影响间隙大小。

子组件越宽,剩余空间越少,间隙越小。固定子组件宽度可以让布局更可预测。

事实五:padding 会影响 SpaceBetween 的计算。

左右 padding 消耗了容器的有效宽度,间接影响了间隙的大小。在设置 padding 时应将其纳入布局规划中。

事实六:SpaceBetween 与 SpaceAround / SpaceEvenly 容易混淆。

三者的核心区别在于「两端是否贴边」:

SpaceBetween: 首尾贴边,中间等间距
SpaceAround:  四周环绕,两端间距 = 内部间距 / 2
SpaceEvenly:  完全等距,所有间距(含两端)相等

13.3 与 Column 的对称使用

SpaceBetween 不仅适用于 Row,也适用于 Column。在 Column 容器中,主轴变为垂直方向,效果变为「首尾贴顶贴底,中间等间距」:

Column() {
  Text('顶部').fontSize(14)
  Text('中部').fontSize(14)
  Text('底部').fontSize(14)
}
.justifyContent(FlexAlign.SpaceBetween)
.height('100%')

这在垂直方向需要「顶、中、底」三端对齐的场景非常有用,比如弹窗中的「标题 - 内容 - 按钮」布局。


十四、常见错误与调试指南

14.1 常见错误

错误 1:只放了一个子组件,却期待两端对齐效果

Row() {
  Text('只有一项').fontSize(14)
}
.justifyContent(FlexAlign.SpaceBetween)  // ❌ 只有一个组件时无效

修复:确认至少有两个子组件,或者使用 layoutWeight(1) 搭配其他组件。

错误 2:混用 SpaceBetween 和固定 margin 导致间距错乱

Row() {
  Text('左').margin({ right: 20 })  // ❌ 手动设置 margin 会破坏 SpaceBetween 的计算
  Text('中')
  Text('右').margin({ left: 20 })
}
.justifyContent(FlexAlign.SpaceBetween)

修复:不要手动设置子组件之间的 margin,完全依赖 SpaceBetween 自动分配间距。如果确实需要额外的间距,在容器的左右 padding 中设置,或者在子组件自己的 padding 中设置。

错误 3:Row 没有设置 width('100%') 导致 SpaceBetween 无效

Row() {
  // 子组件列表...
}
.justifyContent(FlexAlign.SpaceBetween)  // ❌ 如果 Row 宽度等于子组件总宽度,没有剩余空间

修复:设置 width('100%') 让 Row 撑满父容器宽度,或者设置一个固定的宽值。

错误 4:padding 值过大导致子组件被挤压

Row() {
  // 4 个子组件,每个宽度 80vp,总宽 320vp
}
.justifyContent(FlexAlign.SpaceBetween)
.width('100%')
.padding({ left: 60, right: 60 })  // ❌ padding 120vp,剩余空间可能为负

修复:确保 Wavailable = W - PL - PR > Wchildren,否则间距为负,子组件会重叠。

14.2 调试策略

策略一:使用彩色背景标记容器边界

Row() {
  // 子组件...
}
.backgroundColor('#ff000020')  // 半透明红色背景,显示容器范围
.border({ width: 1, color: '#ff0000' })  // 红色边框

通过背景色和边框,可以直观地看到 Row 容器的实际边界,判断 SpaceBetween 是否在预期的范围内分配间距。

策略二:检查子组件的实际宽度

如果 SpaceBetween 的效果不符合预期,先检查子组件的实际宽度。可以使用临时文字显示子组件的宽度:

Row() {
  // 在每个子组件后面加一个显示宽度的 Text
  Column() {
    Text('内容')
    Text('W:80').fontSize(8).fontColor('#999')  // 调试用
  }
}

策略三:逐层排查法

当 SpaceBetween 的效果完全出乎意料时,采用「由外而内」的逐层排查:

  1. 先确认外层 Row 的宽度是否符合预期(width('100%') 是否生效)
  2. 再检查左右 padding 是否合理
  3. 然后确认子组件数量(至少 2 个)
  4. 最后验证子组件的实际宽度之和是否小于 Row 的可用宽度

十五、最佳实践与延伸学习

15.1 SpaceBetween 的最佳实践

实践一:优先使用 SpaceBetween 替代手动 margin

在需要左右两边各放一个组件时,很多新手的直觉是给左右组件设置 margin: auto,但在 ArkTS 中这不可行。正确的做法是用 SpaceBetween 搭配两个子组件。

实践二:固定宽度子组件 + SpaceBetween 组合效果最佳

当子组件宽度固定时,SpaceBetween 的间距计算结果最为可预测。如果子组件宽度由内容撑开,在不同数据下布局效果会变化。

实践三:SpaceBetween 与 layoutWeight 配合使用

有时候需要部分子组件弹性伸缩、部分子组件固定宽度并靠边排列:

Row() {
  // 弹性占位
  Column().layoutWeight(1)

  // 固定宽度的靠右组件
  Row() {
    Button('取消')
    Button('确定')
  }
}
.justifyContent(FlexAlign.SpaceBetween)  // 将空占位和按钮分别推到两端

实践四:适当时使用 SpaceAround 替代 SpaceBetween

当设计稿要求导航栏两端也有留白时(通常是视觉平衡需求),SpaceAroundSpaceEvenly 是更好的选择。

15.2 与其他布局模式的组合

SpaceBetween 很少独立使用,通常与其他布局模式组合:

组合模式 外层 内层 典型场景
SpaceBetween + Column Row (SpaceBetween) 子组件内部用 Column 底部导航栏(图标在上,文字在下)
SpaceBetween + SpaceBetween Row (SpaceBetween) 内层 Row (SpaceBetween) 信息栏(作者
SpaceBetween + layoutWeight Row (SpaceBetween) 某个子组件设置 layoutWeight 搜索栏(输入框弹性 + 按钮固定)
SpaceBetween + Center Row (SpaceBetween) 某个子组件内部 Center 混合布局

15.3 延伸学习路线

掌握了 SpaceBetween 之后,可以进一步探索以下布局模式:

方向 关键 API 学习建议
SpaceAround justifyContent(FlexAlign.SpaceAround) 与 SpaceBetween 对比理解「环绕间距」与「两端间距」的区别
SpaceEvenly justifyContent(FlexAlign.SpaceEvenly) 完全等距布局,适合需要严格对称的场景
layoutWeight 子组件.layoutWeight(1) 弹性空间分配,与 SpaceBetween 互补
Column + SpaceBetween Column + justifyContent(SpaceBetween) 垂直方向的两端对齐(顶部-中部-底部)
Flex + wrap Flex + FlexWrap.Wrap 自动换行的弹性布局
Scroll + Row Scroll + Row 可滚动的横向布局,子组件过多时适用

15.4 写在最后

SpaceBetween 是 ArkTS 布局中最常用、最实用的布局模式之一。它的设计哲学可以概括为:让框架帮你算间距,而不是自己写 margin

在 HarmonyOS NEXT 6.1.1(API 24)的布局体系中,SpaceBetweenSpaceAroundSpaceEvenly 共同构成了「空间分配型排列」的完整工具箱。掌握这三种排列方式的区别和适用场景,是写出高质量鸿蒙应用 UI 的关键一步。

从本文的 7 个演示区可以看出,SpaceBetween 的应用场景极为广泛——从最简单的三栏导航标题,到复杂的底部导航栏、数据统计面板、文章信息栏和标签导航。它的核心价值在于用最少的代码实现最稳定的两端对齐布局

最后,留给读者一个思考题:如果将 justifyContent(FlexAlign.SpaceBetween) 应用在 Column() 容器中,并配合 height('100%'),可以实现什么样的布局效果?不妨动手试一试,你会发现同样的 API 在 Row 和 Column 中展现出的对称之美。

Logo

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

更多推荐