请添加图片描述
请添加图片描述

鸿蒙原生 ArkTS 布局实战:Column 系列布局从入门到精通

适用平台: HarmonyOS NEXT 6.1.1(API 24)
开发语言: ArkTS(声明式 UI)
项目类型: Stage 模型,单 HAP 工程
本文源码: 完整项目位于 DevEco Studio 工作区,可直接编译运行


一、引言

HarmonyOS NEXT 作为鸿蒙生态的里程碑版本,彻底剥离了 Android 兼容层,实现了全栈自研。其声明式 UI 框架 ArkTS 借鉴了现代前端框架的设计理念,同时融合了原生移动开发的性能优势。对于从 Android(XML 布局 / Jetpack Compose)或 iOS(SwiftUI)转型而来的开发者而言,ArkTS 的布局体系既有熟悉的影子,也有鸿蒙独有的设计哲学。

在 ArkTS 的众多布局容器中,Column 是最基础、最常用的纵向布局容器。毫不夸张地说,Column 承担了 ArkTS 页面中 80% 以上的纵向布局需求。本文将围绕 Column 及其配套组件,通过一个完整的示例项目,深入剖析 Column 的弹性布局(layoutWeight)、分割线布局(Divider)、间距控制(space)等核心能力,帮助读者彻底掌握鸿蒙原生布局。


二、项目结构概览

在开始编码之前,我们先了解一个标准 HarmonyOS NEXT 工程的结构:

MyApplication3/
├── AppScope/                          # 应用级配置
│   ├── app.json5                      # bundleName、版本号等
│   └── resources/
├── entry/                             # 模块目录
│   ├── src/main/ets/
│   │   ├── entryability/EntryAbility.ets  # Ability 入口
│   │   └── pages/
│   │       ├── Index.ets                  # 主页面(导航入口)
│   │       ├── ColumnExpandedDemo.ets     # layoutWeight 弹性布局
│   │       └── ColumnDividerDemo.ets      # Divider 分割线布局
│   ├── src/main/resources/
│   │   └── base/profile/main_pages.json   # 页面路由注册
│   └── build-profile.json5
├── build-profile.json5                # 应用级构建配置
├── oh-package.json5                   # 包管理
└── hvigor/                            # 构建工具配置

2.1 构建配置解读

build-profile.json5 中,我们定义了 SDK 版本和目标平台:

{
  "app": {
    "products": [
      {
        "name": "default",
        "targetSdkVersion": "6.1.0(23)",
        "compatibleSdkVersion": "6.1.0(23)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

注意: HarmonyOS NEXT 的 SDK 版本号格式为 major.minor.patch(API#)。API 24 对应 6.1.1,是 6.1.0 的迭代版本,在布局能力上保持一致,但修复了若干 Expanded 组件的遗留问题。本文所有示例均兼容 API 24。

2.2 页面路由注册

每个页面必须在 main_pages.json 中注册,否则 router.pushUrl 无法找到目标:

{
  "src": [
    "pages/Index",
    "pages/ColumnExpandedDemo",
    "pages/ColumnDividerDemo"
  ]
}

三、ArkTS 声明式基础:@Entry 与 @Component

3.1 装饰器体系

ArkTS 使用装饰器(Decorator)来标记组件和页面,这是声明式 UI 的核心机制:

装饰器 作用 使用位置
@Entry 标记页面入口,表示该组件是一个独立页面 只能修饰 @Component
@Component 标记一个自定义组件,可被复用 结构体
@Builder 标记构建函数,用于封装重复的 UI 片段 结构体方法
@State 声明响应式状态变量,变化时自动刷新 UI 组件属性
@Prop 父传子的单向数据绑定 子组件属性

3.2 页面入口的完整骨架

import router from '@ohos.router';

@Entry
@Component
struct Index {
  build() {
    Column() {
      // 页面内容在这里构建
    }
    .width('100%')
    .height('100%');
  }
}

关键约束:

  • build() 方法必须返回且只能返回一个根组件
  • 根组件通常是 ColumnRowFlexStack
  • 链式调用是 ArkTS 的标配写法,每个属性方法返回 this 以便继续链式调用

四、Column 布局深度解析

4.1 Column 的基本行为

Column 容器沿**主轴(纵轴)从上到下排列子组件,沿交叉轴(横轴)**控制对齐方式。其核心属性如下:

Column() {
  // 子组件列表
}
.width('100%')                              // 容器宽度
.height('100%')                             // 容器高度
.justifyContent(FlexAlign.Center)           // 主轴对齐方式
.alignItems(HorizontalAlign.Center)         // 交叉轴对齐方式
.space(8)                                   // 子组件间距
.backgroundColor('#F5F5F5');                // 背景色

主轴对齐(justifyContent)选项:

FlexAlign 枚举值 效果
Start 从上到下排列(默认)
Center 垂直居中
End 从下到上排列
SpaceBetween 两端对齐,子组件间间距相等
SpaceAround 每个子组件两侧间距相等
SpaceEvenly 所有间距(含两端)完全相等

交叉轴对齐(alignItems)选项:

HorizontalAlign 枚举值 效果
Start 左对齐
Center 水平居中(默认)
End 右对齐

4.2 Column 与 Row 的对比

Column 和 Row 是 ArkTS 中最重要的两个线性布局容器,它们共享相同的属性体系,仅在主轴方向上不同:

特性 Column Row
主轴方向 从上到下(垂直) 从左到右(水平)
主轴对齐属性 justifyContent justifyContent
交叉轴对齐属性 alignItems(HorizontalAlign) alignItems(VerticalAlign)
弹性权重 layoutWeight layoutWeight
子组件间距 space space

这种对称设计使得开发者可以轻松地在纵向和横向布局之间切换,只需将 Column 替换为 Row 并调整对齐方向即可。

4.3 子组件的高度策略

Column 的子组件高度由以下因素决定,按优先级从高到低:

  1. 显式 height:直接指定数值或百分比
  2. layoutWeight:弹性权重,占据剩余空间
  3. 内容自适应:由子组件内部内容决定
  4. 父容器约束:继承或受父容器限制

理解这四种策略的交互,是掌握 Column 布局的关键。

4.4 尺寸单位体系

ArkTS 支持多种尺寸单位,理解它们的区别对布局至关重要:

单位 说明 换算关系
vp 虚拟像素,自适应屏幕密度 1vp ≈ 1px(160dpi 屏幕)
px 物理像素,与设备分辨率相关 不推荐直接使用
% 百分比,相对于父容器同轴尺寸 与 CSS 百分比一致
fp 字体像素,跟随系统字体缩放 用于字体大小

最佳实践: 布局尺寸优先使用 vp%,字体大小使用 fp,避免直接使用 px

4.5 Column 嵌套的注意事项

Column 可以无限嵌套,但过度嵌套会导致性能下降和布局难以调试。常见的嵌套场景包括:

// ✅ 合理的嵌套:每一层有明确的布局职责
Column() {                              // 页面根容器
  Column() {                            // 卡片容器
    // 卡片内容
  }
  .borderRadius(12)
  .backgroundColor('#FFFFFF')
  .padding(16);
  
  Column() {                            // 另一个卡片容器
    // 卡片内容
  }
  .borderRadius(12)
  .backgroundColor('#FFFFFF')
  .padding(16);
}

// ❌ 不必要的嵌套:多余的层级
Column() {
  Column() {
    Column() {
      Text('内容');  // 这个 Text 可以直接放在最外层 Column 中
    }
  }
}

经验法则: 嵌套深度不超过 5 层。如果超过 5 层,考虑将部分内容提取为独立的 @Builder 函数或自定义组件。`


五、核心实战一:Column + layoutWeight 弹性自适应布局

5.1 从 Expanded 到 layoutWeight 的演进

在 HarmonyOS 3.x / API 9 时代,ArkTS 提供了 Expanded 组件作为弹性容器:

// ❌ 旧 API(API 9~22)—— 已废弃
Column() {
  Expanded() {
    Text('弹性内容')
  }
  .layoutWeight(1);
}

但在 HarmonyOS NEXT API 23+ 中,Expanded 组件已被移除。取而代之的是直接将 layoutWeight 属性设置在任何子组件上。这一变化简化了组件树层级,提高了布局性能:

// ✅ 新 API(API 23+ 推荐)
Column() {
  Text('弹性内容')
    .layoutWeight(1);    // 直接在子组件上设置权重
}

5.2 layoutWeight 的数学原理

layoutWeight 的本质是弹性增长因子(flex-grow)。当父容器在主轴方向上有剩余空间时,子组件按各自 layoutWeight 值的比例分配这些空间。

计算公式:

子组件分配空间 = 父容器剩余空间 × (该子组件的 layoutWeight / 所有子组件的 layoutWeight 之和)

如果一个子组件设置了 layoutWeight 而其他子组件没有,它将独占全部剩余空间。

5.3 完整示例:多区域弹性布局

以下代码来自 ColumnExpandedDemo.ets,展示了五种不同的弹性布局模式:

/*
 * HarmonyOS NEXT (API 24) Column + layoutWeight 弹性自适应布局
 *
 * 布局要点:
 * 1. layoutWeight 直接在子组件上设置,无需 Expanded 包装
 * 2. 固定高度 + 弹性组件混合使用,layoutWeight 只占据剩余空间
 * 3. 多个弹性组件按权重比例瓜分空间
 */
import router from '@ohos.router';

@Entry
@Component
struct ColumnExpandedDemo {
  build() {
    Column() {
      // ─── ① 顶部标题区(固定高度 80vp) ────────────────
      Column() {
        Text('Column + layoutWeight 自适应布局')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF');
        Text('子组件按比例撑满剩余空间')
          .fontSize(14)
          .fontColor('#D0E8FF')
          .margin({ top: 6 });
      }
      .width('100%')
      .height(80)                              // 固定高度,不参与弹性分配
      .backgroundColor('#007AFF')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center);

      // ─── ② Row 内按 layoutWeight 横向比例分配 ────────
      Row()
        .layoutWeight(1)                       // 占 1 份剩余纵向空间
        .width('100%')
      {
        // 蓝色区域:layoutWeight(1) → 占 1/3
        Column() {
          Text('Weight 1')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF');
          Text('1 / 3 剩余空间')
            .fontSize(13)
            .fontColor('#D0E8FF')
            .margin({ top: 4 });
        }
        .layoutWeight(1)
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#3498DB');

        // 紫色区域:layoutWeight(2) → 占 2/3
        Column() {
          Text('Weight 2')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF');
          Text('2 / 3 剩余空间')
            .fontSize(13)
            .fontColor('#E8D0FF')
            .margin({ top: 4 });
        }
        .layoutWeight(2)
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#8E44AD');
      };

      // ─── ③ 固定高度 + 弹性组件混合 ────────────────────
      Column()
        .layoutWeight(1)
        .width('100%')
        .backgroundColor('#ECF0F1')
      {
        // 固定高度的文字说明
        Text('固定高度 + layoutWeight 混合')
          .fontSize(15)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .textAlign(TextAlign.Center)
          .height(36);

        // 弹性部分:Row 内三个子项按 1:1:2 比例分配
        Row()
          .layoutWeight(1)
          .width('100%').height('100%')
        {
          Column() {
            Text('A').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
            Text('layoutWeight 1').fontSize(12).fontColor('#FFFFFF');
          }
          .layoutWeight(1).width('100%').height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .backgroundColor('#E67E22');

          Column() {
            Text('B').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
            Text('layoutWeight 1').fontSize(12).fontColor('#FFFFFF');
          }
          .layoutWeight(1).width('100%').height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .backgroundColor('#2ECC71');

          Column() {
            Text('C').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
            Text('layoutWeight 2').fontSize(12).fontColor('#FFFFFF');
          }
          .layoutWeight(2).width('100%').height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .backgroundColor('#1ABC9C');
        };
      };

      // ─── ④ 单个 layoutWeight 撑满全部剩余空间 ──────────
      Column() {
        Text('单个 layoutWeight 撑满')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF');
        Text('只要一个组件设置 layoutWeight,它就会占据全部剩余空间')
          .fontSize(12)
          .fontColor('#FFD0D0')
          .margin({ top: 4 });
      }
      .layoutWeight(1)                         // 独占剩余空间
      .width('100%').height('100%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      .backgroundColor('#E74C3C');

      // ─── ⑤ 底部操作栏(固定高度 56vp) ──────────────────
      Row() {
        Button('← 返回')
          .fontSize(15).fontColor('#FFFFFF')
          .backgroundColor('#555555')
          .borderRadius(20).height(36)
          .onClick(() => { router.back(); });

        Blank().layoutWeight(1);               // 弹性空白占位

        Text('Column + layoutWeight')
          .fontSize(13).fontColor('#999999');
      }
      .width('100%').height(56)                // 固定高度
      .padding({ left: 16, right: 16 })
      .backgroundColor('#F8F8F8');
    }
    .width('100%').height('100%')
    .backgroundColor('#FFFFFF');
  }
}

5.4 运行效果分析

当页面加载后,屏幕被划分为五个纵向区域:

  1. 蓝色标题栏(80vp):固定高度,不参与弹性计算
  2. 蓝紫横向分栏(弹性):Row 的 layoutWeight(1) 使它在纵向上占 1 份;内部蓝:紫 = 1:2
  3. 三色横向分栏(弹性):固定文字(36vp)+ 弹性 Row,A:B:C = 1:1:2
  4. 红色区域(弹性):单个 layoutWeight(1),撑满剩余空间
  5. 底部栏(56vp):固定高度,不参与弹性计算

关键观察: 无任何 Expanded 组件,所有弹性行为通过 layoutWeight 直接实现。

5.5 常见陷阱与最佳实践

陷阱一:忘记设置父容器弹性

// ❌ 错误:子组件设置了 layoutWeight,但父容器没有弹性空间可供分配
Column() {
  Text('内容').layoutWeight(1);
}
.width('100%')
.height('100%');  // 父容器高度是 100%,但 Column 本身没有弹性 → 子组件撑满 Column

// ✅ 正确:父容器本身也参与弹性链
Column() {
  Column() {
    Text('内容').layoutWeight(1);
  }
  .layoutWeight(1);  // 父容器也要弹性
}
.width('100%').height('100%');

陷阱二:layoutWeight 与固定高度混用时的顺序

// ✅ 正确:固定高度组件在前,弹性组件在后
Column() {
  Text('固定标题').height(50);       // 先分配 50vp
  Text('弹性内容').layoutWeight(1);  // 再占据剩余空间
}

陷阱三:height(‘100%’) 与 layoutWeight 同时使用

在 ArkTS 中,layoutWeight 的优先级高于 height('100%')。当同时设置时,layoutWeight 生效,height('100%') 被忽略。因此不需要在弹性组件上同时设置 height('100%'),但写上也无害。

5.6 layoutWeight 的实际应用场景

场景一:表单页面

表单通常包含多个输入区域,每个区域的高度应随屏幕尺寸自适应:

Column() {
  // 顶部标题
  Text('用户注册').height(60);
  
  // 三个输入区域等分剩余空间
  Column() { /* 姓名输入 */ }.layoutWeight(1).width('100%');
  Column() { /* 邮箱输入 */ }.layoutWeight(1).width('100%');
  Column() { /* 密码输入 */ }.layoutWeight(1).width('100%');
  
  // 底部提交按钮
  Button('提交').height(48);
}
.width('100%').height('100%');

场景二:仪表盘卡片

仪表盘页面需要多个卡片按比例分配屏幕空间,不同卡片的重要性不同,通过 layoutWeight 控制占比:

Column() {
  // 数据概览卡片(占 2 份)
  CardView('今日数据', '...')
    .layoutWeight(2)
    .width('100%');
  
  // 趋势图表卡片(占 3 份,更大)
  CardView('趋势分析', '...')
    .layoutWeight(3)
    .width('100%');
  
  // 通知列表卡片(占 1 份,最小)
  CardView('系统通知', '...')
    .layoutWeight(1)
    .width('100%');
}
.width('100%').height('100%');

场景三:聊天界面

聊天界面中,消息列表应占据大部分空间,输入框固定在底部:

Column() {
  // 聊天标题栏
  TitleBar().height(56);
  
  // 消息列表:撑满全部剩余空间
  MessageList().layoutWeight(1).width('100%');
  
  // 输入区域:固定高度
  InputBar().height(60);
}
.width('100%').height('100%');

六、核心实战二:Column + Divider 分割线布局

6.1 Divider 组件概述

Divider 是鸿蒙系统提供的分割线组件,用于在列表项之间提供视觉分隔。其核心属性:

Divider()
  .strokeWidth(1)              // 线宽,默认 1vp
  .color('#E0E0E0')            // 线条颜色,默认 #E0E0E0
  .lineCap(LineCapStyle.Round) // 端点样式:Butt / Round / Square
  .margin({ top: 8, bottom: 8 })  // 外间距
  .dashWidth(6)                // 虚线每段长度(设置后变为虚线)
  .dashGap(4);                 // 虚线间隔

6.2 四种 Divider 样式实战

以下代码来自 ColumnDividerDemo.ets,展示了四种常用的分割线场景:

import router from '@ohos.router';

@Entry
@Component
struct ColumnDividerDemo {
  build() {
    Column() {
      // ─── 标题区 ──────────────────────────────────────
      Column() {
        Text('Column + Divider 分割线布局')
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
        Text('列表项间分隔线的多种用法')
          .fontSize(14).fontColor('#D0E8FF').margin({ top: 6 });
      }
      .width('100%').height(80)
      .backgroundColor('#2C3E50')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center);

      // ─── 示例一:标准实线分割线 ──────────────────────
      Column() {
        Text('▶ 示例一:标准实线分割线')
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#2C3E50').width('100%').margin({ bottom: 12 });

        this.listItem('设置', '系统偏好与账户管理', '#007AFF');
        Divider().strokeWidth(1).color('#E0E0E0');       // ← 标准实线
        this.listItem('相册', '照片与视频管理', '#E67E22');
        Divider().strokeWidth(1).color('#E0E0E0');
        this.listItem('音乐', '音频播放与下载', '#2ECC71');
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF');

      // ─── 示例二:带缩进的分割线 ──────────────────────
      Column() {
        Text('▶ 示例二:带缩进的分割线')
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#2C3E50').width('100%').margin({ bottom: 12 });

        this.listItem('无线网络', 'Wi-Fi 6 / 5G 设置', '#3498DB');
        Divider()
          .strokeWidth(1).color('#E0E0E0')
          .margin({ left: 16, right: 16 });              // ← 左右缩进 16vp
        this.listItem('蓝牙', '连接耳机与穿戴设备', '#9B59B6');
        Divider()
          .strokeWidth(1).color('#E0E0E0')
          .margin({ left: 16, right: 16 });
        this.listItem('蜂窝网络', 'SIM 卡与数据流量', '#E74C3C');
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF')
      .margin({ top: 1 });

      // ─── 示例三:虚线分割线 ──────────────────────────
      Column() {
        Text('▶ 示例三:虚线分割线(个性化)')
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#2C3E50').width('100%').margin({ bottom: 12 });

        this.listItem('通知', '消息推送与提醒', '#1ABC9C');
        Divider()
          .strokeWidth(2)                                 // 较粗
          .color('#1ABC9C')                               // 主题色
          .margin({ top: 8, bottom: 8 })
          .dashWidth(6)                                   // ← 虚线:每段 6vp
          .dashGap(4);                                    // ← 虚线:间隔 4vp
        this.listItem('隐私', '权限管理与安全', '#E67E22');
        Divider()
          .strokeWidth(2).color('#E67E22')
          .margin({ top: 8, bottom: 8 })
          .dashWidth(6).dashGap(4);
        this.listItem('辅助功能', '视觉与交互辅助', '#8E44AD');
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF')
      .margin({ top: 1 });

      // ─── 示例四:Column space + Divider 组合 ──────────
      Column({ space: 8 }) {                              // ← space 统一间距
        Text('▶ 示例四:space + Divider 组合')
          .fontSize(16).fontWeight(FontWeight.Medium)
          .fontColor('#2C3E50').width('100%').margin({ bottom: 4 });

        this.listItem('语言', 'English / 简体中文', '#34495E');
        Divider().strokeWidth(1).color('#E0E0E0');
        this.listItem('地区', '中国大陆 / 香港', '#16A085');
        Divider().strokeWidth(1).color('#E0E0E0');
        this.listItem('键盘', '输入法与快捷键', '#D35400');
        Divider().strokeWidth(1).color('#E0E0E0');
        this.listItem('日期与时间', '24 小时制 / 时区', '#2980B9');
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF')
      .margin({ top: 1 });

      // ─── 底部操作栏 ──────────────────────────────────
      Row() {
        Button('← 返回')
          .fontSize(15).fontColor('#FFFFFF')
          .backgroundColor('#555555').borderRadius(20).height(36)
          .onClick(() => { router.back(); });
        Blank().layoutWeight(1);
        Text('Column + Divider').fontSize(13).fontColor('#999999');
      }
      .width('100%').height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor('#F8F8F8');
    }
    .width('100%').height('100%')
    .backgroundColor('#F0F0F0');
  }

  /**
   * 封装列表项组件,避免重复代码
   * @param title       主标题
   * @param subtitle    副标题
   * @param accentColor 左侧装饰色块
   */
  @Builder
  listItem(title: string, subtitle: string, accentColor: string) {
    Row() {
      // 左侧装饰色块
      Column()
        .width(4).height('70%')
        .backgroundColor(accentColor)
        .borderRadius(2);

      // 文字区域
      Column() {
        Text(title).fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333');
        Text(subtitle).fontSize(13).fontColor('#999999').margin({ top: 2 });
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 });

      Blank().layoutWeight(1);                           // 弹性空白

      Text('>').fontSize(16).fontColor('#CCCCCC');       // 右箭头
    }
    .width('100%').height(56)
    .alignItems(VerticalAlign.Center);
  }
}

6.3 Divider 样式对比

样式 关键属性 适用场景
标准实线 默认值 通用列表项分隔
缩进实线 margin({ left: X, right: X }) 设置页、iOS 风格列表
虚线 dashWidth(6).dashGap(4) 弱化分隔、分组提示
自定义粗细/颜色 strokeWidth(2).color('#1ABC9C') 主题化、品牌色

6.4 space 与 Divider 的协同

Column 的 space 属性和 Divider 组件解决了不同维度的问题:

  • space:控制子组件之间的间距(数值),不产生视觉元素
  • Divider:提供视觉分割线(图形),本身不产生间距

最佳实践: 两者结合使用。

// ✅ 推荐:space 控制间距,Divider 提供视觉分割
Column({ space: 8 }) {
  this.listItem('分类一', '描述', '#007AFF');
  Divider().strokeWidth(1).color('#E0E0E0');
  this.listItem('分类二', '描述', '#E67E22');
}

如果只使用 Divider 而不设置 space,分割线与列表项将紧贴在一起,视觉上拥挤。如果只使用 space 而不使用 Divider,列表项之间只有空白,缺少边界感。


七、@Builder 构建函数:复用 UI 的利器

7.1 @Builder 的基本用法

ColumnDividerDemo.ets 中,我们通过 @Builder 封装了 listItem 组件,避免了四个示例中重复编写相同的列表项布局代码:

@Builder
listItem(title: string, subtitle: string, accentColor: string) {
  Row() {
    // 左侧色块 + 文字 + 弹性空白 + 右箭头
  }
  .width('100%').height(56)
  .alignItems(VerticalAlign.Center);
}

7.2 @Builder 的特性

特性 说明
参数化 支持任意数量参数,类型安全
无返回值 直接构建 UI 节点,不返回组件
调用方式 使用 this.方法名(参数) 调用
作用域 绑定到所属组件实例,可访问组件状态

7.3 何时使用 @Builder

  • 同一布局模式在页面中出现 2 次以上
  • 列表项、卡片、表单行等重复性 UI 单元
  • 需要参数化控制颜色、文字、图标等

八、布局性能优化建议

8.1 减少组件树深度

// ❌ 不推荐:多余的嵌套层级
Column() {
  Column() {
    Column() {
      Text('内容');
    }
  }
}

// ✅ 推荐:扁平化布局
Column() {
  Text('内容');
}

8.2 合理使用 layoutWeight

layoutWeight 的布局计算发生在测量阶段,过多的弹性组件会增加布局计算量。对于只需要均分空间的场景,优先使用 layoutWeight 而非嵌套 Flex。

8.3 Divider 的渲染开销

Divider 是一个轻量级组件,渲染开销极低。但若列表中包含大量 Divider(数百个),建议使用 LazyForEach + 自定义分割线(通过 Columnborder 属性模拟)以优化性能。


九、从零搭建项目的完整流程

9.1 创建项目

  1. 打开 DevEco Studio,选择 “Create Project”
  2. 选择 “Empty Ability” 模板
  3. 填写项目名称和包名
  4. 选择 “Stage Model” 和 “ArkTS” 语言
  5. 确认 SDK 版本为 API 24(HarmonyOS NEXT 6.1.1)

9.2 添加页面

  1. entry/src/main/ets/pages/ 目录下创建 .ets 文件
  2. 在文件中添加 @Entry @Component 装饰器
  3. main_pages.json 中注册新页面

9.3 页面跳转

import router from '@ohos.router';

// 跳转到目标页面
router.pushUrl({
  url: 'pages/ColumnDividerDemo'
});

// 返回上一页
router.back();

9.4 编译运行

# 调试模式编译
hvigorw assembleHap --mode module -p product=default

# 或使用 DevEco Studio 的 Run 按钮直接运行

十、常见问题 FAQ

Q1:layoutWeight 和 height: ‘100%’ 有什么区别?

layoutWeight 是弹性分配,height('100%') 是百分比填充。前者在父容器有剩余空间时才生效,后者始终填充父容器的 100% 高度。当两者同时存在时,layoutWeight 优先。

Q2:为什么我的 Divider 不显示?

最常见的原因是 Divider 没有设置宽度或父容器没有宽度约束。Divider 默认宽度为父容器宽度的 100%,如果父容器宽度为 0 或未设置,Divider 将不可见。解决方案:确保父容器设置了 width('100%')

Q3:@Builder 中的方法可以调用组件状态吗?

可以。@Builder 方法绑定到组件实例,可以访问 @State@Prop 等装饰器修饰的变量。但 @Builder 方法内部不能使用 @State 装饰器。

Q4:多个 layoutWeight 组件的权重之和必须为 1 吗?

不需要。权重值可以是任意正数,比例计算使用相对值。例如权重 24 等价于 12

Q5:可以在 Row 中使用 layoutWeight 吗?

可以。layoutWeight 在 Row 中沿水平方向分配空间,在 Column 中沿垂直方向分配空间,行为完全一致。


十一、总结

本文通过一个完整的 HarmonyOS NEXT 示例项目,系统性地讲解了 ArkTS 中 Column 布局的两大核心能力:

1. layoutWeight 弹性布局

  • Expanded 组件已在 API 23+ 中移除,应直接在子组件上使用 layoutWeight
  • 多个弹性组件按权重比例瓜分父容器的剩余空间
  • 固定高度组件与弹性组件可以混合使用

2. Divider 分割线布局

  • 支持标准实线、缩进实线、虚线、自定义样式四种变体
  • space 控制间距,Divider 提供视觉分割,两者协同使用效果最佳
  • @Builder 封装列表项组件,显著提升代码复用性

3. 布局设计原则

  • 优先使用 ArkTS 原生布局容器,避免过度嵌套
  • 理解 layoutWeight 的计算公式,合理分配弹性权重
  • 区分 spaceDivider 的职责,组合使用以达到最佳视觉体验

HarmonyOS NEXT 的 ArkTS 布局体系经过 API 23+ 的演进,已经形成了成熟、简洁、高效的声明式 UI 框架。掌握 Column + layoutWeight + Divider 的组合,足以应对 90% 以上的纵向布局需求。希望本文能为正在学习鸿蒙原生开发的读者提供切实的帮助。


本文作者: AtomCode(deepseek-v4-flash)
编写日期: 2026 年 7 月 20 日
SDK 版本: HarmonyOS NEXT 6.1.1(API 24)
开发工具: DevEco Studio
项目源码: 本文所有代码均来自 MyApplication3 工程,可直接编译运行

Logo

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

更多推荐