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

一、引言:HarmonyOS NEXT 布局体系概览

鸿蒙操作系统自诞生之初就将「弹性布局」作为 UI 开发的核心范式。在 HarmonyOS NEXT(API 24)中,ArkTS 作为主力声明式 UI 语言,提供了一整套强大的布局容器,其中 Column 是最基础、最高频使用的垂直排列容器。

与传统的 Android LinearLayout(垂直)或 iOS UIStackView.vertical)相比,ArkTS 的 Column 在设计上有三个鲜明的特点:

特性 Column(ArkTS) Android LinearLayout iOS UIStackView
声明方式 函数调用式链式配置 XML / 代码 代码 / Storyboard
交叉轴对齐 alignItems(ItemAlign.X) gravity alignment
主轴对齐 justifyContent(FlexAlign.X) gravity(垂直) distribution
响应式更新 @State 自动驱动重绘 notifyDataSetChanged layoutIfNeeded

Column 的精髓在于将「主轴」(main axis)与「交叉轴」(cross axis)彻底分离,使开发者能够以极少的代码精准控制子组件的排列和对齐方式。


在继续深入之前,有必要先理解 ArkTS 声明式 UI 的核心理念。与命令式编程(如 Java Android 的 ViewGroup.addView())不同,ArkTS 采用描述即界面的范式:你描述 UI 应该是什么样子,框架负责将其渲染到屏幕上。当状态变化时,框架自动计算新旧 VNode 树的差异(Diff),仅更新变化的部分,而非重建整个界面。

这种声明式范式与 Column 布局的结合,使得开发者只需关注两点:

  1. 组件树的静态结构 — 哪些子组件在 Column 中,它们如何排列;
  2. 状态驱动的动态变化 — 哪些数据变化会触发布局更新。

这正是 ColumnCenter 布局优雅的根源:居中布局的描述仅需一行 .alignItems(ItemAlign.Center),其余工作全部由框架完成。

二、Column 容器核心机制详解

2.1 主轴与交叉轴

在 Column 中,主轴(Main Axis)永远是垂直方向(从上到下),而交叉轴(Cross Axis)是水平方向(从左到右)。这个概念理解透了,Column 的所有布局属性就不难掌握了。

        ←── 交叉轴(水平)──→
        ┌────────────────────┐
     ↑  │                    │
     │  │   [子组件 A]       │
     主  │                    │
    轴  │   [子组件 B]       │
    (  │                    │
    垂  │   [子组件 C]       │
    直  │                    │
     )  └────────────────────┘
     ↓

2.2 alignItems — 交叉轴对齐

alignItems 控制子组件在水平方向的对齐方式,它接受 ItemAlign 枚举值:

// ItemAlign 枚举的四种取值:
enum ItemAlign {
  Start,    // 左对齐(默认值)
  Center,   // 居中对齐 ★ 本示范核心 ★
  End,      // 右对齐
  Stretch   // 拉伸至填满容器宽度
}

四种对齐效果示意(假设容器宽 300px,子组件宽 100px):

Start:        Center:          End:            Stretch:
┌────┬───┐    ┌────┬───┐    ┌────┬───┐    ┌──────────┐
│A   │   │    │  A │   │    │   A│   │    │A         │
│    │   │    │    │   │    │    │   │    │          │
│B   │   │    │  B │   │    │   B│   │    │B         │
│    │   │    │    │   │    │    │   │    │          │
│C   │   │    │  C │   │    │   C│   │    │C         │
└────┴───┘    └────┴───┘    └────┴───┘    └──────────┘

2.3 justifyContent — 主轴对齐

justifyContent 控制子组件在垂直方向的间距分布方式:

// FlexAlign 枚举的六种取值:
enum FlexAlign {
  Start,        // 从顶部开始排列(默认)
  Center,       // 整体垂直居中
  End,          // 从底部开始排列
  SpaceBetween, // 两端对齐,项目之间间距相等
  SpaceAround,  // 每一项两侧间距相等
  SpaceEvenly   // 所有间距(包括两端)完全相等
}

六种分布效果示意:

Start:     Center:    End:       SpaceBetween: SpaceAround:   SpaceEvenly:
┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐    ┌───────┐     ┌───────┐
│ A     │  │       │  │       │  │ A     │    │       │     │       │
│ B     │  │ A     │  │       │  │       │    │ A     │     │ A     │
│ C     │  │ B     │  │       │  │ B     │    │       │     │       │
│       │  │ C     │  │ A     │  │       │    │ B     │     │ B     │
│       │  │       │  │ B     │  │ C     │    │       │     │       │
│       │  │       │  │ C     │  └───────┘    │ C     │     │ C     │
└───────┘  └───────┘  └───────┘               └───────┘     └───────┘

2.4 Column 的默认 behavior

如果不显式设置 alignItemsjustifyContent,Column 的默认行为是:

  • alignItems(ItemAlign.Start) — 子组件靠左对齐
  • justifyContent(FlexAlign.Start) — 从顶部开始排列

这意味着默认情况下 Column 等同于「ColumnStart」布局。要从「Start」切换到「Center」,只需添加 .alignItems(ItemAlign.Center) 一行代码。

2.5 alignItems 与 justifyContent 的协同工作

理解二者如何协同,是掌握 Column 布局的关键。用一个具体的例子来说明:

假设有一个 Column 容器,高度为 600px,包含三个高度各为 100px 的子组件,容器设置了 alignItems(ItemAlign.Center) 和不同的 justifyContent

// 场景一:justifyContent(Start) + alignItems(Center)
// ┌────────────────────┐
// │       ┌─────┐      │  ← 子组件 A(紧贴顶部)
// │       │  A  │      │
// │       └─────┘      │
// │       ┌─────┐      │
// │       │  B  │      │  ← 子组件 B(紧随 A)
// │       └─────┘      │
// │       ┌─────┐      │
// │       │  C  │      │  ← 子组件 C(紧随 B)
// │       └─────┘      │
// │                    │  ← 剩余空白在底部
// └────────────────────┘

// 场景二:justifyContent(Center) + alignItems(Center)
// ┌────────────────────┐
// │                    │  ← 空白均匀分布在顶部和底部
// │       ┌─────┐      │
// │       │  A  │      │
// │       └─────┘      │
// │       ┌─────┐      │  ← 三个子组件作为一个整体垂直居中
// │       │  B  │      │
// │       └─────┘      │
// │       ┌─────┐      │
// │       │  C  │      │
// │       └─────┘      │
// │                    │
// └────────────────────┘

// 场景三:justifyContent(SpaceEvenly) + alignItems(Center)
// ┌────────────────────┐
// │       ┌─────┐      │  ← 间距 1(与顶部)
// │       │  A  │      │
// │       └─────┘      │
// │       ┌─────┐      │  ← 间距 2(与 A 和 B 之间)
// │       │  B  │      │
// │       └─────┘      │
// │       ┌─────┐      │  ← 间距 3(与 B 和 C 之间)
// │       │  C  │      │
// │       └─────┘      │
// │       ┌─────┐      │  ← 间距 4(与底部)
// └────────────────────┘
// 所有间距(包括两端)完全相等

可以看到,alignItems 控制的是每个子组件在水平方向上的位置(左右),而 justifyContent 控制的是子组件组在垂直方向上的分布(上下)。二者正交,互不干扰。

2.6 子组件的尺寸如何影响居中布局

ColumnCenter 布局中,子组件的宽度设置直接影响居中效果:

// 情况 A:子组件有固定宽度 → 整体居中显示
Text('居中文字')
  .width(120)  // 固定宽度
// 效果:120px 宽的文本块在 Column 中居中

// 情况 B:子组件 width('100%') → 视觉上撑满,居中无意义
Text('撑满')
  .width('100%')  // 撑满父容器
// 效果:宽度与 Column 等宽,居中属性视觉上无变化

// 情况 C:子组件无宽度设置(固有宽度)→ 按内容宽度居中
Text('自适应')
// 效果:文本内容宽度决定宽度,整体居中

这就是为什么在 ColumnCenter 布局中,白底卡片通常使用固定宽度(如 width(140)),而作为背景的容器使用 width('100%') 撑满。


三、项目结构总览与搭建

3.1 项目目录结构

MyApplication/
├── entry/src/main/ets/
│   ├── entryability/
│   │   └── EntryAbility.ets          # 应用入口,加载 ColumnCenterPage
│   └── pages/
│       ├── Index.ets                 # 初始页面(可选)
│       ├── ColumnStartPage.ets       # ColumnStart 对比示例
│       └── ColumnCenterPage.ets      # ★ 本文核心示例 ★
├── entry/src/main/resources/
│   └── base/profile/
│       └── main_pages.json           # 页面路由注册
└── entry/src/main/module.json5       # 模块配置

3.2 入口配置

首先在 EntryAbility.ets 中加载我们即将创建的 ColumnCenterPage

// entry/src/main/ets/entryability/EntryAbility.ets
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  // ... 生命周期方法 ...

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(0x0000, 'testTag', 'Ability onWindowStageCreate');
    
    // ★ 关键:加载 ColumnCenter 布局演示页面
    windowStage.loadContent('pages/ColumnCenterPage', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag',
          'Failed to load content. Cause: %{public}s', JSON.stringify(err));
      }
    });
  }
}

然后在 main_pages.json 中注册页面路由:

{
  "src": [
    "pages/ColumnCenterPage",
    "pages/ColumnStartPage"
  ]
}

四、数据模型设计

在构建 UI 之前,先定义清晰的数据模型。这不仅是好的工程实践,也能让 Column 的渲染逻辑更清晰。

4.1 联系人接口 — ContactItem

联系人卡片需要展示:头像(emoji 占位)、姓名、职位和在线状态。

/**
 * 单个联系人/用户卡片的数据结构
 */
interface ContactItem {
  id: number;
  /** 用户头像(emoji 占位) */
  avatar: string;
  /** 用户姓名 */
  name: string;
  /** 职位或描述 */
  role: string;
  /** 在线状态 */
  online: boolean;
}

4.2 商品接口 — ProductItem

商品列表需要展示:图标、商品名称和价格。

/**
 * 商品项数据结构
 */
interface ProductItem {
  id: number;
  /** 商品图标 */
  icon: string;
  /** 商品名称 */
  name: string;
  /** 价格 */
  price: string;
}

选择接口(interface)而非类(class)的原因:

  • 接口在编译时零开销,不生成任何运行时代码;
  • 接口天然支持结构类型匹配,与 JSON 数据源无缝兼容;
  • 对于纯数据载体,接口比类更简洁。

五、主页面框架:ColumnCenter 三层结构

5.1 组件装饰器与状态变量

使用 @Entry 标记页面入口,@Component 定义组件,@State 声明响应式状态:

@Entry
@Component
struct ColumnCenterPage {
  /* ─── 联系人数据 ─── */
  @State contacts: ContactItem[] = [
    { id: 1, avatar: '👨‍💻', name: '王小明', role: '前端工程师', online: true },
    { id: 2, avatar: '👩‍🎨', name: '李芳', role: 'UI 设计师', online: true },
    { id: 3, avatar: '🧑‍🔧', name: '赵刚', role: '后端工程师', online: false },
    { id: 4, avatar: '👩‍💼', name: '陈静', role: '产品经理', online: true },
    { id: 5, avatar: '🧑‍💻', name: '刘洋', role: '测试工程师', online: false }
  ];

  /** 商品数据 */
  @State products: ProductItem[] = [
    { id: 1, icon: '📱', name: 'HarmonyOS 手机', price: '¥3,999' },
    { id: 2, icon: '💻', name: 'MateBook 笔记本', price: '¥5,999' },
    { id: 3, icon: '⌚', name: '智能手表 Pro', price: '¥1,299' },
    { id: 4, icon: '🎧', name: '降噪耳机', price: '¥899' },
    { id: 5, icon: '📟', name: '智能平板', price: '¥2,499' }
  ];

  /** 当前选中的标签页索引 */
  @State activeTabIndex: number = 0;

  /** 多步骤表单 — 当前步骤 */
  @State currentStep: number = 1;

  /** 表单 — 评分 */
  @State rating: number = 3;

  /** 表单 — 反馈内容 */
  @State feedbackText: string = '';

  /** 表单 — 提交状态 */
  @State submitStatus: string = '';
}

关于 @State 装饰器,这里有三个关键点值得注意:

  1. 响应式绑定@State 修饰的变量发生变化时,框架自动触发 build() 方法重新执行,仅更新有变化的部分(VNode diff)。
  2. 类型推断:ArkTS 支持从初始化值自动推断类型,但显式标注类型更安全,尤其在接口场景下。
  3. 数组响应式@State 修饰的数组,其元素的增删改都会触发 UI 更新,这得益于 ArkTS 对数组操作的深层代理(Proxy-based reactivity)。

深入理解 @State 的响应式原理,对 ColumnCenter 布局的正确使用至关重要。当一个 @State 变量变化时,ArkTS 框架会执行以下过程:

状态变化 → 标记组件为 dirty → 调度异步重绘 →
  执行 build() 生成新 VNode Tree →
    Diff 新旧 VNode Tree →
      只更新差异部分到真实 DOM

这个过程是高效的,因为它避免了全量重绘。但开发者需要注意:不要在一个 @State 中存储过多数据,否则每次细微变化都会导致整棵子树重新 diff。对于大型列表,应该使用 @ObjectLink@Observed 对数据做更细粒度的观察。

在 ColumnCenter 布局中,典型的 @State 使用模式有三类:

// 模式一:数据列表(驱动列表渲染)
@State contacts: ContactItem[] = [ /* ... */ ];

// 模式二:选择状态(驱动条件渲染和样式变化)
@State activeTabIndex: number = 0;

// 模式三:表单输入(双向数据绑定)
@State feedbackText: string = '';

这三种模式覆盖了 ColumnCenter 布局中绝大多数的交互场景。掌握它们,就能应对大部分实际开发需求。

5.2 build() 方法:ColumnCenter 的骨架

build() 方法是 ArkTS 组件的唯一入口,所有 UI 描述必须在此方法内构建:

build() {
  // ★★★ 最外层 Column:整个页面纵向排列,所有子组件居中对齐 ★★★
  Column() {
    // ── 1. 页面标题(居中) ──
    this.buildTitleBar()

    // ── 2. 标签切换栏 ──
    this.buildTabBar()

    // ── 3. 内容展示区(根据标签切换不同内容) ──
    if (this.activeTabIndex === 0) {
      this.buildContactList()       // 3a. 联系人列表
    } else if (this.activeTabIndex === 1) {
      this.buildProductList()       // 3b. 商品推荐列表
    } else {
      this.buildCenterForm()        // 3c. 居中反馈表单
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F5F5')
  // ★★★ 核心布局属性 ★★★
  .alignItems(ItemAlign.Center)     // 交叉轴(水平)→ 居中对齐
  .justifyContent(FlexAlign.Start)  // 主轴(垂直)→ 从顶部排列
}

这里有一个容易被忽略的重要设计原则:主容器的 alignItems(ItemAlign.Center) 定义了整页的「居中基调」。所有直接子组件在水平方向上都居中对齐。如果某个子组件需要打破这个规则(比如标签栏需要左右撑满),可以通过子组件自身的 width('100%') 来覆盖。


六、标题栏:Column 嵌套实现多层次居中

6.1 标题栏的实现

标题栏展示了 Column 嵌套使用的典型场景:外层 Column 负责整体布局,内层 Column 负责内容居中对齐

@Builder
buildTitleBar() {
  Column() {
    // 主标题
    Text('ColumnCenter 布局演示')
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1A1A1A')

    // 副标题 — 显示核心布局参数
    Text('Column + alignItems(Center) + justifyContent')
      .fontSize(12)
      .fontColor('#888888')
      .margin({ top: 4 })

    // 布局要点提示气泡
    Text('💡 所有子组件水平居中')
      .fontSize(12)
      .fontColor('#007AFF')
      .backgroundColor('#E8F4FF')
      .borderRadius(12)
      .padding({ left: 12, right: 12, top: 4, bottom: 4 })
      .margin({ top: 8 })
  }
  .width('100%')
  .padding({ top: 48, bottom: 12 })
  .backgroundColor('#FFFFFF')
  .alignItems(ItemAlign.Center)  // ★ 内部 Column 居中
}

6.2 分层剖析

可以从三个层次理解这个标题栏:

视觉层:三个 Text 组件垂直堆叠,依次显示主标题、副标题、提示气泡。提示气泡使用 backgroundColor('#E8F4FF')borderRadius(12) 模拟了 iOS 风格的小标签。

布局层:内部 Column 设置了 .alignItems(ItemAlign.Center),使得三个 Text 在水平方向都居中。注意这里没有设置 justifyContent,因此默认从顶部开始排列,通过 margin({ top: ... }) 控制垂直间距。

容器层.width('100%') 让标题栏撑满父容器的宽度,.padding({ top: 48 }) 为状态栏预留空间(鸿蒙状态栏高度一般为 44~48px)。


七、标签切换栏:Row 与 Column 的配合

7.1 标签栏整体布局

标签栏使用 Row 实现水平排列,三个标签均匀居中分布:

@Builder
buildTabBar() {
  Row() {
    this.tabButton('👥 联系人', 0)
    this.tabButton('🛒 商品推荐', 1)
    this.tabButton('📝 反馈表单', 2)
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 12, bottom: 12 })
  .backgroundColor('#FFFFFF')
  .justifyContent(FlexAlign.Center)  // ★ Row 内子组件居中对齐
}

这里 RowjustifyContent(FlexAlign.Center) 使三个标签按钮整体居中。Row 的主轴是水平方向,因此 justifyContent 控制的是水平分布,这与 Column 正好相反。

7.2 单个标签按钮

标签按钮是一个带底部指示线的 Column 组件:

@Builder
tabButton(label: string, index: number) {
  Column() {
    Text(label)
      .fontSize(14)
      .fontColor(this.activeTabIndex === index ? '#007AFF' : '#666666')
      .fontWeight(this.activeTabIndex === index ?
        FontWeight.Medium : FontWeight.Regular)

    // 激活状态下显示底部指示线
    if (this.activeTabIndex === index) {
      Divider()
        .width('60%')
        .height(3)
        .color('#007AFF')
        .borderRadius(2)
        .margin({ top: 4 })
    }
  }
  .alignItems(ItemAlign.Center)  // 标签文字和指示线居中
  .onClick(() => {
    this.activeTabIndex = index;  // 切换标签
  })
  .padding({ left: 20, right: 20 })
}

交互逻辑:点击标签时,this.activeTabIndex 更新,触发 build() 重新执行。条件判断 if (this.activeTabIndex === index) 控制底部指示线的显隐,同时控制文字颜色和粗细的变化。

居中继承链:外层 Column → alignItems(Center) → 标签 Column → alignItems(Center) → Text。每一层都显式设置了居中,确保了布局的确定性和可预测性。


八、联系人列表:Column + Scroll 实现居中名片

8.1 列表容器

联系人列表需要滚动能力,因此包裹在 Scroll 中:

@Builder
buildContactList() {
  Scroll() {
    Column() {
      Text('👋 团队成员')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 12 })

      ForEach(this.contacts, (item: ContactItem) => {
        this.contactCard(item)
      }, (item: ContactItem) => item.id.toString())
    }
    .width('100%')
    .padding(16)
    .alignItems(ItemAlign.Center)  // ★ 所有联系卡片居中对齐
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 12 })
  .height(380)
}

Scroll 组件是 ArkTS 中实现内容滚动的标准容器。它与 Column 配合时有一个重要约束:Scroll 的直接子组件必须有确定的高度或使用固有尺寸,否则滚动范围无法计算。这里内部 Column 的子项(联系人卡片)都有固定的 140px 宽度和由内容撑起的高度,因此可以正常工作。

8.2 联系人卡片:Column 居中布局的完整演示

每张联系人卡片是 ColumnCenter 布局的最佳范例

@Builder
contactCard(item: ContactItem) {
  Column() {
    // 头像
    Text(item.avatar)
      .fontSize(36)

    // 姓名
    Text(item.name)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .fontColor('#1A1A1A')
      .margin({ top: 8 })

    // 职位
    Text(item.role)
      .fontSize(13)
      .fontColor('#888888')
      .margin({ top: 4 })

    // 在线状态标签
    Text(item.online ? '🟢 在线' : '🔴 离线')
      .fontSize(12)
      .fontColor(item.online ? '#34C759' : '#999999')
      .margin({ top: 6 })
  }
  .width(140)
  .padding({ top: 20, bottom: 20 })
  .backgroundColor('#FFFFFF')
  .borderRadius(16)
  .margin({ bottom: 12 })
  .alignItems(ItemAlign.Center)  // ★ 卡片内所有子组件居中对齐
  .shadow({
    radius: 8,
    color: 'rgba(0,0,0,0.06)',
    offsetX: 0,
    offsetY: 2
  })
}

居中布局的精髓在这里得到了最充分的体现:所有子组件——从 emoji 头像到姓名、职位、状态标签——都沿着垂直轴居中对齐,形成了一条完美的「视觉中轴线」。这在用户资料展示、个人名片等场景中极其常见。

8.3 卡片的视觉层次设计

卡片使用 borderRadius(16) 实现圆角效果,再通过 shadow() 属性添加阴影:

.shadow({
  radius: 8,
  color: 'rgba(0,0,0,0.06)',
  offsetX: 0,
  offsetY: 2
})

shadow 的四个参数含义:

参数 说明
radius 8 模糊半径,越大阴影越柔和扩散
color rgba(0,0,0,0.06) 阴影颜色,透明度 6% 营造微阴影效果
offsetX 0 水平偏移,0 表示居中阴影
offsetY 2 垂直偏移,正数向下偏移产生「悬浮」感

这种微阴影设计在当前的主流 UI 设计规范(Material Design 3 / 鸿蒙 UX 规范)中被广泛使用,能有效提升卡片的层次感和可点击性。

8.4 ForEach 的 key 生成策略

buildContactList() 中,我们使用 ForEach 遍历联系人数组:

ForEach(this.contacts, (item: ContactItem) => {
  this.contactCard(item)
}, (item: ContactItem) => item.id.toString())

第三个参数 (item: ContactItem) => item.id.toString() 是 key 生成函数。理解它的作用至关重要:

为什么需要 key?

contacts 数组发生变化时(比如新增、删除、重排序),框架需要确定哪些元素是新增的、哪些是已有的、哪些被移除了。key 就是框架做这个「元素身份识别」的依据。

选择 key 的原则:

  1. 稳定性:key 在组件的整个生命周期中不应变化 — item.id 是最佳选择;
  2. 唯一性:同一数组中不能有重复的 key — 这也是为什么使用 id 而不是 index
  3. 可预测性:每次渲染时,同一个数据项应生成相同的 key。

使用 index 作 key 的陷阱:

// ❌ 不要这样写——使用 index 作 key
ForEach(this.contacts, (item, index) => {
  this.contactCard(item)
}, (item, index) => index.toString())

// ✅ 应该使用稳定的 id 作 key
ForEach(this.contacts, (item) => {
  this.contactCard(item)
}, (item) => item.id.toString())

当数组中间插入或删除元素时,index 会发生变化,导致框架误判元素身份,可能引发闪烁、状态丢失等问题。

LazyForEach 何时上场?

如果联系人列表扩展到 50 人以上,就应该使用 LazyForEach 替代 ForEach

// 大数据列表使用 LazyForEach 实现懒加载
class ContactDataSource extends BasicDataSource {
  // ... 实现数据源接口
}

LazyForEach(this.contactDataSource, (item: ContactItem) => {
  this.contactCard(item)
}, (item: ContactItem) => item.id.toString())

LazyForEach 只渲染屏幕上可见的项,屏幕外的项会被回收或延迟创建,这对内存和启动性能都有显著改善。


九、商品推荐列表:Row + Column 混合居中

9.1 商品列表容器

商品列表同样使用 Column + ForEach 的渲染模式:

@Builder
buildProductList() {
  Column() {
    Text('🔥 热销推荐')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .margin({ bottom: 12 })

    ForEach(this.products, (item: ProductItem) => {
      this.productCard(item)
    }, (item: ProductItem) => item.id.toString())
  }
  .width('100%')
  .padding(16)
  .alignItems(ItemAlign.Center)  // ★ 所有商品卡片居中对齐
}

9.2 商品卡片:Row 内的纵向居中

每个商品卡片使用 Row 做水平布局,左侧图标、中间文字、右侧按钮:

@Builder
productCard(item: ProductItem) {
  Row() {
    // 商品图标
    Text(item.icon)
      .fontSize(32)
      .width(56)
      .height(56)
      .backgroundColor('#F8F8F8')
      .borderRadius(28)
      .textAlign(TextAlign.Center)

    // 商品信息(Column 垂直排列,居中)
    Column() {
      Text(item.name)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')

      Text(item.price)
        .fontSize(14)
        .fontColor('#FF3B30')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 4 })
    }
    .alignItems(ItemAlign.Center)  // ★ 商品名和价格居中
    .margin({ left: 16 })

    // 购买按钮
    Button('查看')
      .width(64)
      .height(32)
      .backgroundColor('#007AFF')
      .borderRadius(16)
      .fontSize(12)
      .fontColor('#FFFFFF')
  }
  .width('100%')
  .padding(16)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .margin({ bottom: 10 })
  .alignItems(VerticalAlign.Center)  // ★ Row 内纵向居中
}

这里展现了 Row 与 Column 混合布局的经典模式:

  1. 外层 Row:水平排列图标 → 文字信息 → 按钮。Row 的 alignItems(VerticalAlign.Center) 确保所有子组件在垂直方向居中。
  2. 内层 Column:垂直排列商品名和价格。Column 的 alignItems(ItemAlign.Center) 使文字居中。

这种嵌套模式在处理「图标 + 多行文字 + 操作按钮」的列表项时非常高效,是鸿蒙原生应用中最常用的列表单元设计模式之一。

9.3 商品卡片的视觉层级

商品卡片的设计遵循了视觉层级原则——从上到下、从左到右的信息优先级:

视觉层级(从左到右):
1. 📱 商品图标 → 首先吸引目光(高饱和度 emoji)
2. 「HarmonyOS 手机」→ 商品名称(中等粗细,较大字号)
3. 「¥3,999」→ 价格信息(红色加粗,强调购买诱惑)
4. [查看] 按钮 → 行动号召(蓝色按钮,最终操作目标)

这个层级顺序是经过精心设计的:用户先被图标吸引视线,然后阅读名称确认商品,看到红色价格后做出购买决策,最后点击按钮完成操作。ColumnCenter 的居中排列确保了视觉焦点始终在卡片的中轴线上,引导用户的视线自然流动。

9.4 两种卡片宽度策略的对比

商品卡片使用 width('100%') 撑满父容器宽度,这与联系人卡片的固定宽度策略形成了对比:

策略 固定宽度 撑满宽度
适用场景 网格布局、多列布局 列表布局、单列布局
居中效果 卡片整体在容器中居中 卡片内元素居中
典型示例 联系人名片(8.2 节) 商品列表(本节)
优势 排列整齐,适合多列并行 充分利用屏幕宽度
劣势 大屏下左右留白过多 长文本行较难阅读

9.5 为什么不需要 Scroll?

商品列表没有使用 Scroll 包裹,因为商品数据只有 5 项,内容高度在屏幕可见区域内。如果商品数量扩展到 10 个以上,就需要加上 Scroll:

@Builder
buildProductList() {
  Scroll() {                    // ★ 当数据量大时添加 Scroll
    Column() {
      // ... 列表内容
    }
    .alignItems(ItemAlign.Center)
  }
  .height(380)                  // 限定高度以启用滚动
}

判断是否需要 Scroll 的经验法则:当 Column 的子组件总高度可能超过父容器高度时,就必须使用 Scroll。在鸿蒙开发中,这是一个常见的「溢出」陷阱。


十、多步骤反馈表单:ColumnCenter 的交互实战

表单部分是本示例中最复杂的场景,它展示了 ColumnCenter 在「分步交互」中的应用。整个表单分为三个步骤,通过 currentStep 状态变量控制。

10.1 表单容器

@Builder
buildCenterForm() {
  Scroll() {
    Column() {
      Text('📝 用户反馈')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .margin({ bottom: 8 })

      Text('您的意见对我们很重要')
        .fontSize(13)
        .fontColor('#888888')
        .margin({ bottom: 20 })

      // 步骤进度指示器
      this.buildStepIndicator()

      // 根据步骤显示不同内容
      if (this.currentStep === 1) {
        this.buildStep1Rating()
      } else if (this.currentStep === 2) {
        this.buildStep2Feedback()
      } else {
        this.buildStep3Submit()
      }
    }
    .width('100%')
    .padding(24)
    .alignItems(ItemAlign.Center)  // ★ 所有表单项居中对齐
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 12 })
  .height(380)
}

10.2 步骤进度指示器

步骤指示器使用 Row 水平排列三个步骤圆点,并用 Divider 连接:

@Builder
buildStepIndicator() {
  Row() {
    this.stepDot('1', this.currentStep >= 1, '评分')
    this.stepLine(this.currentStep > 1)
    this.stepDot('2', this.currentStep >= 2, '反馈')
    this.stepLine(this.currentStep > 2)
    this.stepDot('3', this.currentStep >= 3, '完成')
  }
  .width('100%')
  .justifyContent(FlexAlign.Center)  // ★ 步骤条整体居中
  .margin({ bottom: 24 })
}

@Builder
stepDot(step: string, active: boolean, label: string) {
  Column() {
    Text(step)
      .width(32)
      .height(32)
      .backgroundColor(active ? '#007AFF' : '#E0E0E0')
      .borderRadius(16)
      .fontColor('#FFFFFF')
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .textAlign(TextAlign.Center)

    Text(label)
      .fontSize(11)
      .fontColor(active ? '#007AFF' : '#999999')
      .margin({ top: 4 })
  }
  .alignItems(ItemAlign.Center)
}

@Builder
stepLine(active: boolean) {
  Divider()
    .width(40)
    .height(2)
    .color(active ? '#007AFF' : '#E0E0E0')
    .margin({ left: 4, right: 4, bottom: 20 })
}

步骤指示器是一个纯粹的数据驱动组件:它根据 currentStep 的值决定每个步骤圆点的激活状态,并动态改变颜色。连接线同样跟随状态变化,形成完整的视觉反馈链条。

10.3 步骤一:评分

评分组件使用五个 emoji 字符模拟「星评」,点击时更新 rating 值:

@Builder
buildStep1Rating() {
  Column() {
    Text('请为我们的服务评分')
      .fontSize(15)
      .fontColor('#333333')
      .margin({ bottom: 16 })

    // 5 星评分 Row
    Row() {
      ForEach([1, 2, 3, 4, 5], (star: number) => {
        Text(star <= this.rating ? '⭐' : '☆')
          .fontSize(36)
          .onClick(() => {
            this.rating = star
          })
      }, (star: number) => star.toString())
    }
    .justifyContent(FlexAlign.Center)  // ★ 星星居中对齐

    Text(this.getRatingLabel(this.rating))
      .fontSize(14)
      .fontColor('#007AFF')
      .margin({ top: 12 })

    Button('下一步')
      .width(160)
      .height(44)
      .backgroundColor('#007AFF')
      .borderRadius(22)
      .fontColor('#FFFFFF')
      .fontSize(15)
      .margin({ top: 24 })
      .onClick(() => {
        this.currentStep = 2  // 前进到步骤 2
      })
  }
  .width('100%')
  .padding(20)
  .backgroundColor('#FFFFFF')
  .borderRadius(16)
  .alignItems(ItemAlign.Center)  // ★ 评分区域所有元素居中
}

交互流程: 用户点击星星 → this.rating 更新 → 组件重绘,对应星星从 ☆ 变为 ⭐ → 下方文字同步更新评分描述 → 点击「下一步」→ this.currentStep = 2 → 步骤指示器和内容区同时更新。

10.4 步骤二:反馈输入

多行文本输入区配合两个按钮(上一步 / 提交):

@Builder
buildStep2Feedback() {
  Column() {
    Text('请详细描述您的体验')
      .fontSize(15)
      .fontColor('#333333')
      .margin({ bottom: 16 })

    // 多行文本输入
    TextArea({ placeholder: '请输入您的反馈意见...', text: this.feedbackText })
      .width('100%')
      .height(120)
      .backgroundColor('#F8F8F8')
      .borderRadius(12)
      .padding(12)
      .fontSize(14)
      .onChange((value: string) => {
        this.feedbackText = value
      })

    // 按钮组(居中)
    Row() {
      Button('上一步')
        .width(120)
        .height(44)
        .backgroundColor('#E0E0E0')
        .borderRadius(22)
        .fontColor('#666666')
        .fontSize(15)
        .margin({ right: 12 })
        .onClick(() => {
          this.currentStep = 1  // 回退到步骤 1
        })

      Button('提交反馈')
        .width(120)
        .height(44)
        .backgroundColor('#007AFF')
        .borderRadius(22)
        .fontColor('#FFFFFF')
        .fontSize(15)
        .onClick(() => {
          this.currentStep = 3  // 前进到步骤 3
          this.submitStatus = ''
        })
    }
    .justifyContent(FlexAlign.Center)  // ★ 按钮组居中
    .margin({ top: 20 })
  }
  .width('100%')
  .padding(20)
  .backgroundColor('#FFFFFF')
  .borderRadius(16)
  .alignItems(ItemAlign.Center)  // ★ 反馈区域所有元素居中
}

TextArea 是 ArkTS 中用于多行文本输入的组件。它的 onChange 回调在用户输入时触发,将输入值同步到 feedbackText 状态变量中。两个按钮的 onClick 分别实现向前和向后导航。

10.5 步骤三:提交完成

展示成功状态,并提供「再次反馈」按钮重置整个流程:

@Builder
buildStep3Submit() {
  Column() {
    Text('✅')
      .fontSize(48)

    Text('感谢您的反馈!')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1A1A1A')
      .margin({ top: 12 })

    Text('我们会认真阅读每一条意见,持续优化产品体验。')
      .fontSize(13)
      .fontColor('#888888')
      .margin({ top: 8 })
      .textAlign(TextAlign.Center)  // ★ 多行文本居中
      .width('80%')

    if (this.submitStatus) {
      Text(this.submitStatus)
        .fontSize(13)
        .fontColor('#34C759')
        .margin({ top: 8 })
    }

    Button('再次反馈')
      .width(160)
      .height(44)
      .backgroundColor('#007AFF')
      .borderRadius(22)
      .fontColor('#FFFFFF')
      .fontSize(15)
      .margin({ top: 24 })
      .onClick(() => {
        // 重置所有状态
        this.currentStep = 1
        this.rating = 3
        this.feedbackText = ''
        this.submitStatus = ''
      })
  }
  .width('100%')
  .padding(40)
  .backgroundColor('#FFFFFF')
  .borderRadius(16)
  .alignItems(ItemAlign.Center)  // ★ 提交成功页所有元素居中
}

10.6 辅助方法

评分文字描述通过数组映射实现:

getRatingLabel(r: number): string {
  const labels: string[] = [
    '😞 非常不满意',
    '😟 不满意',
    '😐 一般',
    '😊 满意',
    '🤩 非常满意'
  ];
  return labels[r - 1] ?? '';
}

?? 是空值合并运算符,当 r - 1 索引超出数组范围时返回空字符串。这在防御性编程中非常实用,防止 getRatingLabel(0)getRatingLabel(6) 导致运行时崩溃。


十一、ColumnCenter 布局的最佳实践

11.1 何时使用 ColumnCenter

ColumnCenter(Column + alignItems Center)在以下场景中最适合:

场景 示例 推荐原因
卡片式列表 联系人名片、会员卡 每张卡片内部元素居中排列,视觉平衡
表单页面 注册页、登录页、反馈页 表单项居中,整体布局干净整洁
空状态页 "暂无数据"提示 居中的图标 + 文字 + 按钮更醒目
确认页 提交成功、下单成功 居中的成功图标和提示文字
功能入口 首页功能图标区 图标 + 文字垂直居中排列

11.2 避免过度使用

尽管 ColumnCenter 用途广泛,但并非所有场景都适合居中:

  • 长文本阅读页(新闻、文章):正文应左对齐,居中会破坏阅读节奏;
  • 复杂数据表格:数据应按列对齐(通常左对齐),居中导致阅读困难;
  • 多级导航菜单:层级缩进是必要的视觉提示,居中会模糊层级关系。

11.3 使用 @Builder 拆分复杂 Column

当 Column 的子组件超过 5 个时,强制将 UI 拆分为多个 @Builder 方法:

// ❌ 不推荐:将所有 UI 写在一个 build() 中
build() {
  Column() {
    // 第 1 组 UI(标题)
    Text('...')
    Text('...')
    // 第 2 组 UI(输入)
    TextInput()
    Button()
    // 第 3 组 UI(列表)
    ForEach(...)
  }
}

// ✅ 推荐:拆分为 @Builder 方法
build() {
  Column() {
    this.buildHeader()
    this.buildForm()
    this.buildList()
  }
}

@Builder
buildHeader() { /* 标题相关 */ }

@Builder
buildForm() { /* 表单相关 */ }

@Builder
buildList() { /* 列表相关 */ }

@Builder 拆分的三大好处:

  1. 可读性:每个 Builder 只做一件事,命名即文档;
  2. 可复用性:同一个 Builder 可在不同位置调用;
  3. 可测试性:隔离的 UI 片段更易于验证。

11.4 性能优化建议

Column 在处理大量子组件时,需要注意以下几点:

  1. 使用 ForEach + key:始终为 ForEach 提供稳定的 key 生成函数(第三个参数),帮助框架精确追踪元素变化:

    ForEach(items, item => { /* ... */ },
      item => item.id.toString())  // ★ 稳定的 key
    
  2. 避免深层嵌套:Column 嵌套超过 5 层时,考虑提取为独立组件或使用 Grid/List。

  3. 使用 LazyForEach:超过 50 个子项时,使用 LazyForEach 替代 ForEach 实现懒加载渲染。

  4. 合理使用 @Builder:将重复的 UI 结构提取为 @Builder 方法,减少 build() 中的重复代码,提高可维护性。


十二、与其他布局方式的对比

12.1 ColumnStart vs ColumnCenter vs ColumnEnd

这是 Column 交叉轴对齐的三种基本形态:

// ColumnStart — 左对齐(默认)
Column() { /* ... */ }
  .alignItems(ItemAlign.Start)

// ColumnCenter — 居中对齐 ★ 本文主题 ★
Column() { /* ... */ }
  .alignItems(ItemAlign.Center)

// ColumnEnd — 右对齐
Column() { /* ... */ }
  .alignItems(ItemAlign.End)

三种对齐方式的视觉差异:

ColumnStart:     ColumnCenter:     ColumnEnd:
┌────────────┐   ┌────────────┐   ┌────────────┐
│A           │   │     A      │   │           A│
│B           │   │     B      │   │           B│
│C           │   │     C      │   │           C│
└────────────┘   └────────────┘   └────────────┘

12.2 ColumnCenter vs Flex 布局

ArkTS 中 Flex 组件比 Column 更灵活,但 Column 在垂直排列场景下有独特优势:

对比维度 Column Flex
默认方向 垂直 水平
代码简洁性 更简洁(默认垂直) 需要显式 direction
适用场景 纵向列表、表单 复杂弹性布局
性能 轻量 略有开销
换行支持 不支持(Row/Column 单行/列) 支持 wrap
对齐控制 alignItems + justifyContent alignItems + justifyContent(同 Column)

选择建议:80% 的垂直排列场景用 Column 即可,只有需要复杂的换行(wrap)或动态方向时,才切换到 Flex。

12.3 ColumnCenter vs List

List 组件专为长列表优化,支持懒加载和滑动复用。对比:

场景 Column + Scroll List
数据量 < 30 ✅ Column 足够 过度设计
数据量 30~100 可以但可能卡顿 ✅ 推荐
数据量 > 100 ❌ 不推荐 ✅ 必须使用
滑动复用 不支持 原生支持(itemRecycle)
列表项多样性 手动通过 if/else 控制 支持多模板 ListItemGroup
粘性标题 手动实现 原生 sticky 支持
下拉刷新 手动实现 配合 Refresh 组件
开发复杂度

选择建议:

  • 10 项以下:Column + Scroll 最简洁;
  • 10~50 项:如无复杂交互,Column 仍可接受,但 Liist 更专业;
  • 50 项以上:必须使用 List,否则会有帧率问题。

12.4 ColumnCenter vs RelativeContainer

RelativeContainer 是 HarmonyOS NEXT(API 24)引入的相对布局容器,与 ColumnCenter 的设计理念有本质区别:

对比维度 ColumnCenter RelativeContainer
布局本质 自动垂直流式 手动锚点约束
对齐控制 alignItems + justifyContent alignRules + 锚点引用
子组件数量 任意,自动排列 有限,需逐一指定位置
嵌套层级 可多层嵌套 通常 1~2 层
理解难度 低(直觉式) 中高(需理解锚点系统)
适合场景 线性列表、表单、卡片流 复杂相对定位、覆盖层

RelativeContainer 的典型用法是针对「A 在 B 的右侧,C 在 B 的下方」这类非线性的相对关系。对于从上到下的线性布局,ColumnCenter 是更自然、更高效的选择。

// RelativeContainer 示例:复杂的相对定位
RelativeContainer() {
  Button('确认')
    .alignRules({
      bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
      center: { anchor: '__container__', align: HorizontalAlign.Center }
    })
  Button('取消')
    .alignRules({
      bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
      end: { anchor: '__container__', align: HorizontalAlign.End }
    })
}
.width('100%')
.height('100%')

这段代码用 RelativeContainer 实现了两个按钮分别位于底部居中和底部右侧。如果用 ColumnCenter + Row 实现,反而更复杂。因此,选择布局容器应先评估布局需求,再选择最匹配的工具

十三、完整项目代码清单

以下是从项目构建到运行所需的全部关键文件:

13.1 入口模块配置

entry/src/main/ets/entryability/EntryAbility.ets — 应用入口,加载 ColumnCenterPage。

entry/src/main/module.json5 — 模块配置,声明 EntryAbility 和备份 Ability。

entry/src/main/resources/base/profile/main_pages.json — 页面路由表,注册所有页面。

13.2 主页面代码

entry/src/main/ets/pages/ColumnCenterPage.ets — 本文的核心文件,640 行完整代码,包含:

  • 联系人名片列表(ColumnCenter 布局)
  • 商品推荐列表(Row + Column 混合)
  • 多步骤反馈表单(交互式 ColumnCenter)

13.3 对比参考

entry/src/main/ets/pages/ColumnStartPage.ets — ColumnStart(左对齐)的对比示例,帮助理解 Center 与 Start 的差异。


十四、调试与验证

14.1 在 DevEco Studio 中运行

  1. 打开项目,确保 SDK 为 HarmonyOS NEXT 6.1.1(API 24);
  2. entry 模块的 run 配置中选择模拟器或真机;
  3. 点击 Run 按钮,应用启动后应看到标题「ColumnCenter 布局演示」;
  4. 点击三个标签切换联系人、商品推荐和反馈表单。

14.2 常见问题排查

现象 可能原因 解决方案
页面白屏 页面路径未注册 检查 main_pages.json
子组件不居中 忘记设置 alignItems 检查 Column 的链式调用
布局溢出 子组件总高度超过父容器 添加 Scroll 包裹
标签切换无反应 @State 未正确绑定 检查状态变量名是否一致

14.3 效果验证标准

运行应用后,可以逐项验证 ColumnCenter 的布局效果:

  1. 标题栏:三行文字在水平方向完美居中,提示气泡居中对齐;
  2. 标签栏:三个标签等距居中分布,底部指示线居中对齐;
  3. 联系人名片:每张卡片内的头像、姓名、职位、在线状态全部居中对齐,卡片本身在页面中居中排列;
  4. 商品列表:每行商品图标、名称、价格、按钮在垂直方向居中对齐;
  5. 反馈表单:评分星星居中,按钮居中,提交成功页面的图标和文字居中。

十五、总结

本文通过一个结合了联系人名片、商品推荐和多步骤反馈表单的综合性示例应用,深入讲解了 HarmonyOS NEXT(API 24)中 ColumnCenter 布局的完整用法。

回顾核心要点:

  1. Column + alignItems(ItemAlign.Center) 是实现垂直居中对齐布局的黄金组合,一行代码即可将默认的左对齐切换为居中对齐。

  2. 主轴与交叉轴的分离设计是 Column 布局的思想基础:alignItems 控制水平方向(交叉轴),justifyContent 控制垂直方向(主轴)。

  3. @Builder 方法分解是组织复杂 Column 布局的最佳实践,将标题栏、标签栏、内容区分别封装为独立的 Builder 方法,使 build() 逻辑清晰可维护。

  4. @State 驱动的响应式更新使 Column 内的内容切换、状态变化无需手动操作 DOM,框架自动完成差异更新。

  5. Row + Column 混合布局在处理图文混排、列表项时非常高效,外层 Row 水平布局,内层 Column 垂直排列文字信息。

15.1 从本示例中学到的核心经验

通过构建 ColumnCenterPage 这个示例,可以总结出以下可迁移的布局开发经验:

经验一:先定骨架,再填内容。
始终先确定页面最外层的布局容器(Column 还是 Row),设置好主轴和交叉轴的对齐方式,再逐层添加子组件。

经验二:状态驱动优于手动操控。
所有的 UI 变化(标签切换、步骤前进、评分选择)都应通过 @State 变量驱动,而非手动调用 UI 更新方法。

经验三:Builder 方法是最好的注释。
良好的方法命名(buildTitleBarbuildContactListbuildStep1Rating)本身就是文档,比写在注释中的说明更可靠。

经验四:居中对齐需要顶层设计。
如果页面最终需要居中对齐效果,应该在布局的最顶层 Column 就设置 alignItems(ItemAlign.Center),而非在每个子组件中单独做居中补偿。

15.2 后续学习方向

掌握 ColumnCenter 布局之后,可以继续探索以下方向:

  • RowCenter 布局:Row 容器 + alignItems(Center) + justifyContent(Center),实现水平居中排列;
  • Flex 弹性布局:更灵活的 wrap 和动态尺寸控制;
  • Grid 网格布局:二维布局,适合商品展示墙、照片墙;
  • RelativeContainer:锚点相对布局,适合复杂的自定义界面;
  • 自定义布局:实现 Layout 接口,完全掌控子组件的测量和排列。

15.3 写在最后

ColumnCenter 布局是鸿蒙原生应用开发中最基础、最高频的布局模式之一。掌握它,就掌握了鸿蒙 UI 开发的半壁江山。无论是简单的信息展示页,还是复杂的多步骤表单,ColumnCenter 都能提供优雅、简洁的布局方案。

重要的是,ColumnCenter 所体现的「描述式布局」思想——告诉框架「我要居中对齐」,而非「把这个组件移动到屏幕中央」——正是声明式 UI 开发范式的精髓所在。当你在鸿蒙生态中持续深入时,会发现这种思维方式贯穿了所有的容器组件(Row、Flex、Grid、Stack),理解了一个就能触类旁通。

项目完整代码位于 entry/src/main/ets/pages/ColumnCenterPage.ets,欢迎在 DevEco Studio 中打开运行,直观感受 ColumnCenter 的布局效果。

Logo

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

更多推荐