鸿蒙原生 ArkTS 布局之 ColumnStart 垂直排列深度解析


1. 前言:为什么需要 ColumnStart?
在移动端应用开发中,纵向列表和垂直排列是最基础也是最常见的 UI 布局需求。无论是微信的消息列表、淘宝的商品瀑布流,还是各类 App 的注册登录表单,其底层布局架构都可以归结为「子组件自上而下依次排列」。
HarmonyOS NEXT 作为华为自研的全场景操作系统,其原生 UI 框架 ArkUI 提供了一系列声明式布局容器。其中 Column 是最核心的垂直布局容器,而 alignItems(ItemAlign.Start) + justifyContent(FlexAlign.Start) 的组合则解决了绝大多数纵向页面的布局诉求。
我们称这种组合为 ColumnStart —— 它代表一种「垂直排列,顶部对齐,靠左对齐」的布局范型。本文将通过一个完整的实战项目,从零到一剖析 ColumnStart 布局的方方面面。
2. Column 布局容器概述
2.1 什么是 Column?
Column 是 ArkUI 中最基础的布局容器之一,其核心特点是:所有子组件在主轴(垂直方向)上从上到下依次排列。可以将其类比为前端 Flexbox 布局中的 flex-direction: column,或者 Android 原生开发中的 LinearLayout(orientation: vertical)。
2.2 Column 与 Row 的对比
| 容器 | 主轴方向 | 交叉轴方向 | 典型场景 |
|---|---|---|---|
| Column | 垂直(从上到下) | 水平(从左到右) | 列表、表单、信息流 |
| Row | 水平(从左到右) | 垂直(从上到下) | 导航栏、按钮组、标签栏 |
2.3 Column 的声明语法
在 ArkTS 中,Column 以闭包形式使用:
Column() {
// 子组件按顺序添加到 Column 中
Text('第一个子组件')
Text('第二个子组件')
Button('第三个子组件')
}
// 容器级属性通过链式调用设置
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
2.4 Column 的核心控制属性
Column 容器通过两个关键属性控制其子组件的排列布局:
alignItems—— 控制子组件在**交叉轴(水平方向)**上的对齐方式justifyContent—— 控制子组件在**主轴(垂直方向)**上的间距分布
这正是 ColumnStart 布局的精髓所在:两方面同时控制,实现精准的垂直排列。
3. alignItems 与 justifyContent 深入理解
理解这两个属性是掌握 Column 布局的关键。我们用一张概念图来厘清它们的职责:
┌─────────────────────────────────────┐
│ Column 容器 │
│ justifyContent 控制这个方向 ↓ │
│ (主轴 · 垂直) │
│ │
│ ← alignItems 控制这个方向 → │
│ (交叉轴 · 水平) │
│ │
│ ┌─────────────────────────┐ │
│ │ 子组件 A │ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ 子组件 B │ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ 子组件 C │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────┘
3.1 alignItems 详解
alignItems 作用于 交叉轴(Cross Axis)。对于 Column 来说,交叉轴就是水平方向。它决定了子组件在水平方向上的对齐行为。
ArkUI 提供了四种对齐选项:
| 枚举值 | 效果 | 说明 |
|---|---|---|
ItemAlign.Start |
靠左对齐 | 所有子组件在容器左侧边缘对齐(默认行为) |
ItemAlign.Center |
居中对齐 | 所有子组件在容器水平中央对齐 |
ItemAlign.End |
靠右对齐 | 所有子组件在容器右侧边缘对齐 |
ItemAlign.Stretch |
拉伸填充 | 子组件在水平方向拉伸至填满容器宽度(默认行为,若不设置则默认为 Stretch) |
3.2 justifyContent 详解
justifyContent 作用于 主轴(Main Axis)。对于 Column 来说,主轴就是垂直方向。它决定了子组件在垂直方向上的间距分布方式。
ArkUI 提供了以下选项:
| 枚举值 | 效果 | 说明 |
|---|---|---|
FlexAlign.Start |
顶部对齐 | 所有子组件从容器顶部开始排列(默认行为) |
FlexAlign.Center |
垂直居中 | 所有子组件在容器垂直方向中央排列 |
FlexAlign.End |
底部对齐 | 所有子组件从容器底部开始排列 |
FlexAlign.SpaceBetween |
两端对齐 | 第一个子组件在顶部,最后一个在底部,中间等距分布 |
FlexAlign.SpaceAround |
环绕间距 | 每个子组件两侧间距相等 |
FlexAlign.SpaceEvenly |
均匀分布 | 子组件之间及与容器边界的间距均相等 |
4. ItemAlign.Start 与其他对齐方式的对比
为了更直观地理解 ItemAlign.Start 的效果,我们对比四种对齐方式的可视化表现。
4.1 ItemAlign.Start(靠左对齐)
Column() {
Text('短文本')
Text('这是一段比较长的文本内容,长度不同')
Button('按钮')
}
.width('100%')
.backgroundColor('#E8F5E9')
.alignItems(ItemAlign.Start)
效果:所有子组件以左侧为基准对齐。短文本在左,长文本也从左开始,按钮同样靠左。这是阅读习惯中最自然的排版方式,也是绝大多数纵向页面的首选。
4.2 ItemAlign.Center(居中对齐)
.alignItems(ItemAlign.Center)
效果:所有子组件的水平中心线与容器中心对齐。适合标题、提示语等需要视觉居中的场景。但如果子组件宽度不一致,会导致左侧起始位置参差不齐,视觉上反而不够整洁。
4.3 ItemAlign.End(靠右对齐)
.alignItems(ItemAlign.End)
效果:所有子组件以右侧为基准对齐。适用于操作菜单、更多按钮等需要放在右侧的场景,但不适合大面积的文本列表。
4.4 ItemAlign.Stretch(拉伸填充)
.alignItems(ItemAlign.Stretch)
效果:所有子组件在水平方向上拉伸至填满容器的宽度。这是 Column 在未显式设置 alignItems 时的默认行为。适合需要子组件撑满宽度的场景,例如列表项带有底部分隔线、表单输入框需要占满屏幕宽度等。
4.5 选择建议
| 场景 | 推荐的对齐方式 | 原因 |
|---|---|---|
| 消息列表、新闻列表 | ItemAlign.Start |
文本左对齐符合阅读习惯 |
| 表单标签 + 输入框 | ItemAlign.Start |
标签和输入框左对齐,视觉整齐 |
| 模态弹窗内容 | ItemAlign.Center |
让弹窗内容视觉居中 |
| 操作菜单底栏 | ItemAlign.End |
操作按钮聚集在右侧 |
| 卡片式列表项 | ItemAlign.Stretch |
卡片撑满宽度,底部分隔线对齐 |
5. FlexAlign.Start 主轴排列详解
5.1 FlexAlign.Start 的行为
FlexAlign.Start 告诉 Column 容器:所有子组件从顶部开始依次向下排列。如果子组件的总高度小于容器高度,底部会留出空白区域。
Column() {
Text('第一个')
Text('第二个')
Text('第三个')
}
.width('100%')
.height(500)
.backgroundColor('#FFF3E0')
.justifyContent(FlexAlign.Start)
效果:三个 Text 均从容器顶部开始排列,三者在顶部紧密排列,底部留白。
5.2 与其他 FlexAlign 的对比
| 排列方式 | 顶部间距 | 底部间距 | 子组件间距 |
|---|---|---|---|
FlexAlign.Start |
0 | 留白 | 0(紧凑) |
FlexAlign.Center |
等比例 | 等比例 | 0(紧凑) |
FlexAlign.End |
留白 | 0 | 0(紧凑) |
FlexAlign.SpaceBetween |
0 | 0 | 均分剩余空间 |
FlexAlign.SpaceAround |
一半间距 | 一半间距 | 均分剩余空间 |
FlexAlign.SpaceEvenly |
均分 | 均分 | 与边界间距相等 |
5.3 ColumnStart 的完整组合
ColumnStart 布局的完整声明:
Column() {
// 子组件列表
}
.width('100%')
.height('100%')
.alignItems(ItemAlign.Start) // 子组件在水平方向靠左对齐
.justifyContent(FlexAlign.Start) // 子组件在垂直方向从顶部开始排列
这种组合确保了子组件无论数量多少、高度几何,都从左上角原点开始依次排列,是最自然、最符合阅读顺序的布局方式。
6. 项目结构全景概览
在开始编写代码之前,我们先看一下整个项目的目录结构,明确各个文件的职责。
MyApplication
├── AppScope/
│ ├── app.json5 # 应用级配置
│ └── resources/ # 全局资源
├── entry/ # 模块入口
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability 生命周期管理 & 页面加载
│ │ │ └── pages/
│ │ │ ├── Index.ets # 默认首页(本项目中未使用)
│ │ │ └── ColumnStartPage.ets # ★ 核心:ColumnStart 布局演示页
│ │ ├── module.json5 # 模块配置(声明 Ability、页面路由等)
│ │ └── resources/ # 模块级资源
│ └── build-profile.json5 # 构建配置
├── oh-package.json5 # 包管理配置
└── hvigor/ # 构建工具配置
6.1 关键文件职责
- EntryAbility.ets:应用的入口 Ability,负责在
onWindowStageCreate阶段通过loadContent加载ColumnStartPage页面 - ColumnStartPage.ets:核心演示页面,包含三种 ColumnStart 布局场景的完整实现
- module.json5:通过
"pages": "$profile:main_pages"映射到main_pages.json,后者声明了所有可用页面的路由
6.2 main_pages.json 配置
{
"src": [
"pages/ColumnStartPage"
]
}
这意味着应用启动时,框架会根据路由表找到 pages/ColumnStartPage 并将其作为可加载的页面。EntryAbility 中调用 loadContent('pages/ColumnStartPage', callback) 即可加载该页。
7. 从零搭建 ColumnStartPage 页面
下面我们开始构建核心的 ColumnStartPage 页面。整个页面将按照 「外层 Column 容器 → 标题栏 → 标签栏 → 内容区」 的结构进行搭建。
7.1 页面整体布局结构
Column(最外层容器)
├── buildTitleBar() ── 标题栏(内部又是 Column)
├── buildTabBar() ── 标签切换栏(使用 Row 水平排列)
└── 条件渲染内容区
├── [Tab 0] buildMessageList() ── 消息列表
├── [Tab 1] buildFormDemo() ── 注册表单
└── [Tab 2] buildInfoFeed() ── 信息流
7.2 页面入口与装饰器
@Entry
@Component
struct ColumnStartPage {
// 状态变量和构建方法...
}
@Entry:标记该组件为页面的入口组件,表示这是一个完整的页面,而不是一个可复用的子组件@Component:标记该结构体为 ArkUI 组件,使其具备组件化的能力struct ColumnStartPage:使用struct定义组件结构体,这是 ArkTS 自定义组件的标准写法
7.3 build() 方法与 Column 根容器
build() {
Column() {
this.buildTitleBar()
this.buildTabBar()
if (this.activeTabIndex === 0) {
this.buildMessageList()
} else if (this.activeTabIndex === 1) {
this.buildFormDemo()
} else {
this.buildInfoFeed()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.alignItems(ItemAlign.Start)
.justifyContent(FlexAlign.Start)
}
这里最关键的三行代码:
.alignItems(ItemAlign.Start)—— 所有直接子组件(标题栏、标签栏、内容区)在水平方向靠左对齐.justifyContent(FlexAlign.Start)—— 所有直接子组件从容器顶部开始垂直排列.width('100%').height('100%')—— 容器撑满整个屏幕,为子组件提供完整的布局空间
#F5F5F5 的浅灰色背景作为页面底色,让白色卡片内容区自然悬浮在页面上方,形成良好的层次感。
7.4 @Builder 装饰器:构建可复用 UI 片段
在 ArkTS 中,@Builder 装饰器用于定义可复用的 UI 构建函数:
@Builder
buildTitleBar() {
// UI 构建逻辑
}
@Builder 方法需要通过 this.方法名() 的方式在 build() 或另一个 @Builder 方法中调用。使用 @Builder 有以下好处:
- 代码模块化:将大型 UI 拆分为独立的构造块,每个块职责单一
- 可复用性:同一
@Builder可以在多处调用 - 可读性:
build()方法本身变得简洁清晰,只需列出关键模块即可
8. 场景一:消息列表 —— 纯 Column 垂直列表
8.1 数据模型定义
首先定义消息项的数据结构:
interface MessageItem {
id: number;
avatar: string; // 发件人头像(用 emoji 占位)
name: string; // 发件人名称
summary: string; // 消息摘要
time: string; // 时间戳
unread: boolean; // 是否已读
}
使用 interface 定义类型,而非 class,因为这里只需定义数据结构,不需要方法。ArkTS 中的 interface 编译开销更小,更适合纯数据模型。
8.2 状态变量初始化
@State messages: MessageItem[] = [
{ id: 1, avatar: '👤', name: '张三', summary: '项目方案已更新,请查收最新版本', time: '09:32', unread: true },
{ id: 2, avatar: '👥', name: '设计团队', summary: '首页UI切图已上传至共享文件夹', time: '昨天', unread: true },
{ id: 3, avatar: '📋', name: '任务提醒', summary: '你有3个待办事项即将到期', time: '昨天', unread: false },
{ id: 4, avatar: '📢', name: '系统通知', summary: '系统将于本周六02:00-05:00进行维护升级', time: '周三', unread: false },
{ id: 5, avatar: '💬', name: '李四', summary: '收到,我明天下午回复你', time: '周三', unread: false }
];
@State 装饰器标记的变量是响应式的。当 messages 数组内容发生变化时,UI 会自动重新渲染。这是 ArkTS 声明式 UI 的核心机制 —— 数据驱动 UI。
8.3 buildMessageList():消息列表容器
@Builder
buildMessageList() {
Column() {
Text('最近消息')
.fontSize(16)
.fontColor('#333333')
.margin({ left: 4, top: 8, bottom: 4 })
ForEach(this.messages, (item: MessageItem) => {
this.messageItem(item)
}, (item: MessageItem) => item.id.toString())
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
}
这里有几个布局要点:
- 外层 Column:承载整个消息列表区,
alignItems(ItemAlign.Start)使标题和消息条目都靠左对齐 - 白色卡片背景:
backgroundColor('#FFFFFF')+borderRadius(12)创建一个圆角卡片效果 - 外边距:
margin({ left: 16, right: 16, top: 12 })让卡片与屏幕边缘保持间距
8.4 ForEach 循环渲染
ForEach 是 ArkUI 中的列表循环渲染语法:
ForEach(
this.messages, // 数据源数组
(item: MessageItem) => { // UI 生成函数
this.messageItem(item)
},
(item: MessageItem) => item.id.toString() // key 生成函数(用于列表复用优化)
)
第三个参数是 key 生成器,为每个列表项生成唯一标识符。当列表数据发生变化时(增删改),框架根据 key 值精确判断哪些项需要重新渲染,而非刷新整个列表。这能显著提升长列表的性能表现。
8.5 messageItem():单个消息条目的 Column+Row 混合布局
@Builder
messageItem(item: MessageItem) {
Row() {
// 左侧:头像
Text(item.avatar)
.fontSize(28)
.width(48)
.height(48)
.backgroundColor('#F0F0F0')
.borderRadius(24)
.textAlign(TextAlign.Center)
// 右侧:文字区域(嵌套 Column)
Column() {
// 第一行:姓名 + 时间
Row() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#1A1A1A')
Text(item.time)
.fontSize(12)
.fontColor('#AAAAAA')
.margin({ left: 8 })
}
.alignItems(VerticalAlign.Center)
// 第二行:消息摘要
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.margin({ top: 4 })
// 未读标记
if (item.unread) {
Text('● 未读')
.fontSize(11)
.fontColor('#FF3B30')
.margin({ top: 2 })
}
}
.height('100%')
.justifyContent(FlexAlign.Center)
.margin({ left: 12 })
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.borderRadius(8)
}
这是一个典型的 Column + Row 混合布局案例,展示了如何在 Column 容器中嵌套 Row 来实现复杂的 UI 结构。
布局层级分析:
Row(水平:头像 | 文字区域)
├── Text(头像 emoji)
└── Column(垂直:姓名行 | 摘要 | 未读标记)
├── Row(水平:姓名 + 时间)
│ ├── Text(姓名)
│ └── Text(时间)
├── Text(消息摘要)
└── Text(未读标记,条件渲染)
关键布局代码解释:
- 外层
Row将头像和文字区域在水平方向并排排列 - 右侧
Column内部使用垂直排列,但没有显式设置alignItems(ItemAlign.Start),因为 Column 默认行为就是靠左对齐 - 内层
Row(姓名+时间)使用alignItems(VerticalAlign.Center)实现文本在垂直方向居中对齐 .justifyContent(FlexAlign.Center)让文字区域在垂直方向居中,使姓名行和摘要行的整体居于 Row 高度的中央maxLines(1)+textOverflow({ overflow: TextOverflow.Ellipsis })实现单行文本溢出省略号的效果
关于 alignItems 的重要提醒:在 Column 上,alignItems 控制水平对齐;在 Row 上,alignItems 控制垂直对齐。两者方向不同,但概念一致。
9. 场景二:注册表单 —— 纵向表单项排列
9.1 表单字段枚举
enum FormField {
USERNAME = 0,
EMAIL = 1,
PASSWORD = 2,
CONFIRM_PASSWORD = 3
}
使用 enum 枚举来区分不同的表单字段,比用字符串字面量更安全、更可维护。在 onChange 回调中通过枚举值判断当前是哪个字段在变化,避免散布多个独立的回调函数。
9.2 表单状态变量
@State username: string = '';
@State email: string = '';
@State password: string = '';
@State confirmPassword: string = '';
@State formMessage: string = '';
每个表单字段对应一个 @State 变量。formMessage 用于显示验证错误提示信息。当表单提交或字段内容变化时,这些响应式变量驱动 UI 重新渲染。
9.3 buildFormDemo():表单容器
@Builder
buildFormDemo() {
Column() {
Text('用户注册')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.margin({ bottom: 20 })
this.formField('用户名', '请输入用户名', FormField.USERNAME)
this.formField('邮箱地址', '请输入邮箱', FormField.EMAIL)
this.formField('密码', '请输入密码', FormField.PASSWORD)
this.formField('确认密码', '请再次输入密码', FormField.CONFIRM_PASSWORD)
if (this.formMessage.length > 0) {
Text(this.formMessage)
.fontSize(13)
.fontColor('#FF3B30')
.margin({ top: 8, left: 4 })
}
Button('注册')
.width(120)
.height(44)
.backgroundColor('#007AFF')
.borderRadius(22)
.fontColor('#FFFFFF')
.fontSize(16)
.margin({ top: 24 })
.onClick(() => {
this.handleRegister()
})
}
.width('100%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
.alignItems(ItemAlign.Start)
}
布局要点:
alignItems(ItemAlign.Start)确保标题、所有表单项、提示文本以及注册按钮都靠左对齐- 按钮设置了固定宽度
120,而不是撑满父容器,配合左对齐形成「小按钮在左」的视觉效果 - 错误提示文本仅在
formMessage非空时条件渲染
9.4 formField():单个表单项
@Builder
formField(label: string, placeholder: string, field: FormField) {
Column() {
Text(label)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 4 })
TextInput({ placeholder: placeholder })
.width('100%')
.height(44)
.backgroundColor('#F8F8F8')
.borderRadius(8)
.padding({ left: 12 })
.fontSize(14)
.onChange((value: string) => {
if (field === FormField.USERNAME) {
this.username = value
} else if (field === FormField.EMAIL) {
this.email = value
} else if (field === FormField.PASSWORD) {
this.password = value
} else if (field === FormField.CONFIRM_PASSWORD) {
this.confirmPassword = value
}
})
}
.width('100%')
.margin({ bottom: 16 })
}
设计思路:
- 每个表单项是一个独立的
Column容器,上层是标签Text,下层是输入框TextInput - 标签和输入框左对齐,符合表单填写的视觉引导
TextInput宽度设为'100%'撑满父容器,保证在不同屏幕宽度下均能自适应- 背景色
#F8F8F8与纯白卡片背景形成微妙的层次差异,暗示输入框区域是可交互的 - 圆角
borderRadius(8)使输入框风格柔和
9.5 handleRegister():表单验证逻辑
handleRegister(): void {
if (!this.username || !this.email || !this.password || !this.confirmPassword) {
this.formMessage = '请填写所有字段'
return
}
if (this.password !== this.confirmPassword) {
this.formMessage = '两次输入的密码不一致'
return
}
if (this.password.length < 6) {
this.formMessage = '密码长度不能少于6位'
return
}
this.formMessage = ''
console.info(`[ColumnStartDemo] 注册成功: ${this.username}`)
}
这是一个完整的客户端表单验证逻辑:
- 空值检查:所有字段必须填写
- 一致性检查:密码与确认密码必须相同
- 长度检查:密码不能少于 6 位
- 通过处理:清空错误信息,输出成功日志
当验证失败时,this.formMessage 被赋予错误文本。由于 formMessage 是 @State 变量,UI 中的条件渲染 if (this.formMessage.length > 0) 会刷新,红色错误提示文本随即出现。
10. 场景三:信息流 —— 可滚动的 Column 布局
10.1 信息流数据
@State feedItems: string[] = [
'HarmonyOS NEXT 5.0 发布:全面升级原生体验',
'ArkTS 3.0 新特性:更简洁的状态管理',
'鸿蒙生态设备数量突破 10 亿',
'开发者大会 2025 议程公布'
];
这里使用 string[] 简单数组,实际项目中可以替换为更复杂的结构化数据。
10.2 buildInfoFeed():可滚动的 Column
@Builder
buildInfoFeed() {
Scroll() {
Column() {
Text('最新资讯')
.fontSize(16)
.fontColor('#333333')
.margin({ left: 4, bottom: 12 })
ForEach(this.feedItems, (item: string, index: number) => {
Row() {
Text(`${index + 1}`)
.width(24)
.height(24)
.backgroundColor('#007AFF')
.borderRadius(12)
.fontColor('#FFFFFF')
.fontSize(12)
.textAlign(TextAlign.Center)
Text(item)
.fontSize(14)
.fontColor('#333333')
.margin({ left: 12 })
}
.width('100%')
.padding({ top: 10, bottom: 10, left: 4 })
.borderRadius(6)
.alignItems(VerticalAlign.Center)
}, (item: string) => item)
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
.height(300)
}
10.3 Scroll 容器详解
Scroll 是 ArkUI 的滚动容器组件,当内容超出容器尺寸时自动启用滚动能力。
使用 Scroll 的注意点:
- Scroll 只能有一个直接子组件。因此这里 Scroll 内部只放了一个
Column,再由Column承载所有信息流条目 - Column 本身不设置高度,其高度由子内容撑开。Scroll 通过比较 Column 的实际高度与 Scroll 自身的固定高度(300)来判定是否需要滚动
- 如果内容不足一屏,Scroll 不会出现滚动条,表现如同普通 Column
10.4 信息流条目设计
每个信息流条目使用 Row 水平排列序号圆圈和新闻标题:
- 序号圆圈:固定 24×24 大小的圆形蓝色背景,白色数字居中,形成清晰的可视化编号
- 标题文本:靠左排列,与序号保持 12px 间距
.alignItems(VerticalAlign.Center):使序号与文本在垂直方向居中对齐
11. Column + Row 混合布局技巧
在实际项目中,几乎不会只使用纯 Column 布局。Column 与 Row 的混合嵌套是构建复杂 UI 的基础。
11.1 嵌套原则
Column(纵向主框架)
├── Row(导航栏、标签栏)
├── Column(内容区块)
│ ├── Row(区块内的行布局)
│ └── Row(区块内的另一行)
└── Row(底部操作栏)
11.2 本项目中使用的混合布局模式
模式一:Row 内部嵌套 Column
Row() {
// 左侧固定内容
Text('头像')
// 右侧可变内容(用 Column 实现纵向排列)
Column() {
Text('标题行')
Text('详情行')
Text('额外行')
}
}
这种模式适用于「左侧固定图标/头像 + 右侧垂直文字信息」的场景,如联系人列表、消息列表、评论列表等。
模式二:Column 内部嵌套 Row
Column() {
Row() {
Text('姓名')
Text('时间')
}
Text('详细内容')
}
这种模式适用于「标题行(多元素水平排列)+ 详情行(单元素独占一行)」的场景。
11.3 对齐属性的继承与覆盖
重要概念:alignItems 和 justifyContent 是容器属性,只作用于直接子组件,不会递归应用到孙组件。
Column() { // 父 Column:alignItems(Start)
Column() { // 子 Column:没有设置 alignItems → 默认 Stretch
Text('文本') // 孙组件:被拉伸填满子 Column 宽度
}
}
.alignItems(ItemAlign.Start)
这意味着每个容器都需要独立设置其对齐属性,不能依赖父容器的对齐设置传递到后代组件。
12. @Builder 装饰器:复用 Column 布局片段
12.1 @Builder 的基本用法
@Builder 是 ArkTS 中定义可复用 UI 的方法装饰器:
@Builder
buildTitleBar() {
Column() {
Text('ColumnStart 布局演示')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Text('Column + alignItems(Start) + justifyContent')
.fontSize(13)
.fontColor('#888888')
.margin({ top: 4 })
}
.width('100%')
.padding({ left: 20, top: 48, bottom: 12 })
.backgroundColor('#FFFFFF')
.alignItems(ItemAlign.Start)
}
然后在 build() 中调用:
build() {
Column() {
this.buildTitleBar() // ← 调用 @Builder 方法
// 其他内容...
}
}
12.2 @Builder 方法的参数传递
@Builder 方法可以接收参数,使其更加灵活:
@Builder
tabButton(label: string, index: number) {
Column() {
Text(label).fontSize(14)
.fontColor(this.activeTabIndex === index ? '#007AFF' : '#666666')
if (this.activeTabIndex === index) {
Divider().width('80%').height(3).color('#007AFF').borderRadius(2).margin({ top: 4 })
}
}
.onClick(() => {
this.activeTabIndex = index;
})
}
// 调用时传入不同参数
this.tabButton('📬 消息列表', 0)
this.tabButton('📝 注册表单', 1)
this.tabButton('📰 信息流', 2)
12.3 @Builder 与 @Component 的选择
| 特性 | @Builder | @Component |
|---|---|---|
| 定义方式 | 结构体中的方法 | 独立 struct |
| 状态管理 | 访问外部 @State | 拥有自己的 @State |
| 复用范围 | 当前组件内部 | 全局可复用 |
| 性能开销 | 轻量(内联展开) | 较重(独立组件实例) |
| 适用场景 | 页面内部 UI 片段复用 | 独立可复用的组件单元 |
13. @State 状态管理与布局联动
13.1 响应式状态驱动
ArkTS 的 @State 装饰器实现了响应式编程。当状态变量变化时,框架自动追踪依赖该状态的 UI 部分并触发最小粒度的重新渲染。
@State activeTabIndex: number = 0;
当用户点击标签按钮时:
.onClick(() => {
this.activeTabIndex = index; // 状态变化
})
UI 自动更新:
if (this.activeTabIndex === 0) {
this.buildMessageList() // 重新渲染对应内容
} else if (this.activeTabIndex === 1) {
this.buildFormDemo()
} else {
this.buildInfoFeed()
}
同时,标签按钮的样式也随状态变化:
Text(label)
.fontColor(this.activeTabIndex === index ? '#007AFF' : '#666666')
.fontWeight(this.activeTabIndex === index ? FontWeight.Medium : FontWeight.Regular)
13.2 @State 的变更检测
@State 采用浅比较机制来检测变化:
- 对于基本类型(
string、number、boolean),直接比较值是否改变 - 对于引用类型(
Array、Object),比较引用地址是否改变
因此,修改数组元素的属性并不会触发 UI 更新:
// ❌ 不会触发 UI 更新
this.messages[0].unread = false;
// ✅ 会触发 UI 更新
this.messages = [...this.messages]; // 创建新数组
14. 完整的 ColumnStartPage.ets 源码
下面给出项目中所用的完整源码(去除注释后的精简版,但保留了关键的布局属性说明):
@Entry
@Component
struct ColumnStartPage {
/* ─── 状态变量 ─── */
@State messages: MessageItem[] = [
{ id: 1, avatar: '👤', name: '张三', summary: '项目方案已更新,请查收最新版本', time: '09:32', unread: true },
{ id: 2, avatar: '👥', name: '设计团队', summary: '首页UI切图已上传至共享文件夹', time: '昨天', unread: true },
{ id: 3, avatar: '📋', name: '任务提醒', summary: '你有3个待办事项即将到期', time: '昨天', unread: false },
{ id: 4, avatar: '📢', name: '系统通知', summary: '系统将于本周六02:00-05:00进行维护升级', time: '周三', unread: false },
{ id: 5, avatar: '💬', name: '李四', summary: '收到,我明天下午回复你', time: '周三', unread: false }
];
@State activeTabIndex: number = 0;
@State username: string = '';
@State email: string = '';
@State password: string = '';
@State confirmPassword: string = '';
@State formMessage: string = '';
@State feedItems: string[] = [
'HarmonyOS NEXT 5.0 发布:全面升级原生体验',
'ArkTS 3.0 新特性:更简洁的状态管理',
'鸿蒙生态设备数量突破 10 亿',
'开发者大会 2025 议程公布'
];
build() {
Column() {
this.buildTitleBar()
this.buildTabBar()
if (this.activeTabIndex === 0) {
this.buildMessageList()
} else if (this.activeTabIndex === 1) {
this.buildFormDemo()
} else {
this.buildInfoFeed()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.alignItems(ItemAlign.Start) // ★ 核心:子组件靠左对齐
.justifyContent(FlexAlign.Start) // ★ 核心:子组件从顶部排列
}
/* ── 标题栏 ── */
@Builder
buildTitleBar() {
Column() {
Text('ColumnStart 布局演示').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
Text('Column + alignItems(Start) + justifyContent').fontSize(13).fontColor('#888888').margin({ top: 4 })
}
.width('100%')
.padding({ left: 20, top: 48, bottom: 12 })
.backgroundColor('#FFFFFF')
.alignItems(ItemAlign.Start)
}
/* ── 标签栏 ── */
@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.SpaceEvenly)
}
@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('80%').height(3).color('#007AFF').borderRadius(2).margin({ top: 4 })
}
}
.onClick(() => { this.activeTabIndex = index })
}
/* ── 消息列表 ── */
@Builder
buildMessageList() {
Column() {
Text('最近消息').fontSize(16).fontColor('#333333').margin({ left: 4, top: 8, bottom: 4 })
ForEach(this.messages, (item: MessageItem) => { this.messageItem(item) },
(item: MessageItem) => item.id.toString())
}
.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
}
@Builder
messageItem(item: MessageItem) {
Row() {
Text(item.avatar).fontSize(28).width(48).height(48).backgroundColor('#F0F0F0')
.borderRadius(24).textAlign(TextAlign.Center)
Column() {
Row() {
Text(item.name).fontSize(15).fontWeight(FontWeight.Medium).fontColor('#1A1A1A')
Text(item.time).fontSize(12).fontColor('#AAAAAA').margin({ left: 8 })
}.alignItems(VerticalAlign.Center)
Text(item.summary).fontSize(13).fontColor('#666666').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%').margin({ top: 4 })
if (item.unread) {
Text('● 未读').fontSize(11).fontColor('#FF3B30').margin({ top: 2 })
}
}.height('100%').justifyContent(FlexAlign.Center).margin({ left: 12 })
}.width('100%').padding({ top: 12, bottom: 12 }).borderRadius(8)
}
/* ── 注册表单 ── */
@Builder
buildFormDemo() {
Column() {
Text('用户注册').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A1A').margin({ bottom: 20 })
this.formField('用户名', '请输入用户名', FormField.USERNAME)
this.formField('邮箱地址', '请输入邮箱', FormField.EMAIL)
this.formField('密码', '请输入密码', FormField.PASSWORD)
this.formField('确认密码', '请再次输入密码', FormField.CONFIRM_PASSWORD)
if (this.formMessage.length > 0) {
Text(this.formMessage).fontSize(13).fontColor('#FF3B30').margin({ top: 8, left: 4 })
}
Button('注册').width(120).height(44).backgroundColor('#007AFF').borderRadius(22)
.fontColor('#FFFFFF').fontSize(16).margin({ top: 24 })
.onClick(() => { this.handleRegister() })
}
.width('100%').padding(24).backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 16, right: 16, top: 12 }).alignItems(ItemAlign.Start)
}
@Builder
formField(label: string, placeholder: string, field: FormField) {
Column() {
Text(label).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#333333').margin({ bottom: 4 })
TextInput({ placeholder: placeholder }).width('100%').height(44).backgroundColor('#F8F8F8')
.borderRadius(8).padding({ left: 12 }).fontSize(14)
.onChange((value: string) => {
if (field === FormField.USERNAME) { this.username = value }
else if (field === FormField.EMAIL) { this.email = value }
else if (field === FormField.PASSWORD) { this.password = value }
else if (field === FormField.CONFIRM_PASSWORD) { this.confirmPassword = value }
})
}.width('100%').margin({ bottom: 16 })
}
handleRegister(): void {
if (!this.username || !this.email || !this.password || !this.confirmPassword) {
this.formMessage = '请填写所有字段'; return
}
if (this.password !== this.confirmPassword) {
this.formMessage = '两次输入的密码不一致'; return
}
if (this.password.length < 6) {
this.formMessage = '密码长度不能少于6位'; return
}
this.formMessage = ''
console.info(`[ColumnStartDemo] 注册成功: ${this.username}`)
}
/* ── 信息流 ── */
@Builder
buildInfoFeed() {
Scroll() {
Column() {
Text('最新资讯').fontSize(16).fontColor('#333333').margin({ left: 4, bottom: 12 })
ForEach(this.feedItems, (item: string, index: number) => {
Row() {
Text(`${index + 1}`).width(24).height(24).backgroundColor('#007AFF')
.borderRadius(12).fontColor('#FFFFFF').fontSize(12).textAlign(TextAlign.Center)
Text(item).fontSize(14).fontColor('#333333').margin({ left: 12 })
}.width('100%').padding({ top: 10, bottom: 10, left: 4 }).borderRadius(6)
.alignItems(VerticalAlign.Center)
}, (item: string) => item)
}.width('100%')
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 16, right: 16, top: 12 }).height(300)
}
}
interface MessageItem {
id: number;
avatar: string;
name: string;
summary: string;
time: string;
unread: boolean;
}
enum FormField {
USERNAME = 0,
EMAIL = 1,
PASSWORD = 2,
CONFIRM_PASSWORD = 3
}
15. EntryAbility 页面加载配置
应用启动时,EntryAbility 负责加载 ColumnStartPage 页面。
15.1 EntryAbility.ets 源码
import { AbilityConstant, ConfigurationConstant, 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 {
try {
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s',
JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/ColumnStartPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag',
'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
// 其他生命周期方法...
}
15.2 关键调用链
- 应用启动 →
EntryAbility.onCreate()调用 - 窗口创建 →
EntryAbility.onWindowStageCreate()调用 windowStage.loadContent('pages/ColumnStartPage', callback)加载页面- 框架根据
main_pages.json的路由映射找到pages/ColumnStartPage对应的.ets文件 - 页面的
@Entry @Component struct ColumnStartPage被实例化并渲染
15.3 module.json5 配置
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
]
}
}
"skills" 配置中的 entity.system.home 和 ohos.want.action.home 表示该 Ability 是应用的主入口,会在桌面显示应用图标。
16. 常见问题与避坑指南
16.1 Column 的默认 alignItems 是 Stretch
初学者最容易犯的错误是认为 Column 默认左对齐。实际上,Column 在不设置 alignItems 时,默认行为是 ItemAlign.Stretch,即子组件会拉伸填满容器的宽度。
// ❌ 子组件会拉伸,Text 背景会撑满整个宽度
Column() {
Text('左对齐文本')
.backgroundColor('#FFE0E0')
}
.width('100%')
// ✅ 显式设置 Start,Text 背景仅包裹文本内容
Column() {
Text('左对齐文本')
.backgroundColor('#FFE0E0')
}
.width('100%')
.alignItems(ItemAlign.Start)
16.2 alignItems 不影响容器自身
alignItems 控制的是子组件的对齐,不是容器自身的对齐。容器的对齐需要由其父容器的 alignItems 控制。
// 外部 Column 控制内部 Column 的对齐
Column() {
Column() {
// 内部 Column 的内容
}
.width(200) // 显式设置宽度
.backgroundColor('#E0E0FF')
}
.width('100%')
.alignItems(ItemAlign.Center) // 内部 Column 在外部 Column 中居中
16.3 justifyContent 需要确定性的容器高度
justifyContent 的效果依赖于容器的高度。如果容器高度由子组件撑开(即没有显式设置高度),则 justifyContent 的效果不明显,因为剩余空间为 0。
// ❌ justifyContent 无效果——容器高度由子组件撑满
Column() {
Text('A')
Text('B')
}
.alignItems(ItemAlign.Start)
.justifyContent(FlexAlign.SpaceBetween)
// ✅ 容器有固定高度,SpaceBetween 才能生效
Column() {
Text('A')
Text('B')
}
.height(400)
.alignItems(ItemAlign.Start)
.justifyContent(FlexAlign.SpaceBetween)
16.4 ForEach 的 key 生成器不可省略
// ❌ 省略 key 生成器——列表更新时性能差
ForEach(this.messages, (item: MessageItem) => {
this.messageItem(item)
})
// ✅ 提供 key 生成器——框架可以精确追踪每个列表项
ForEach(this.messages, (item: MessageItem) => {
this.messageItem(item)
}, (item: MessageItem) => item.id.toString())
16.5 @Builder 方法不能独立修改状态
@Builder
tabButton(label: string, index: number) {
Column() {
Text(label)
}
// ✅ 正确:通过回调修改组件的 @State
.onClick(() => {
this.activeTabIndex = index
})
// ❌ 错误:@Builder 内部不能使用 @State 装饰器定义新状态
// @State localState: boolean = false
}
16.6 Scroll 容器内不要使用固定高度
Scroll() {
Column() {
// ❌ 不需要给 Column 设置固定高度
// .height('100%') ← 这会破坏滚动
}
}
Scroll 判断是否需要滚动的依据是:子内容的高度 > Scroll 容器的高度。如果子 Column 设置了固定高度等于 Scroll 高度,滚动将不会生效。
17. 总结与最佳实践
17.1 ColumnStart 布局的精髓
ColumnStart 布局的核心可以归结为两行代码:
.alignItems(ItemAlign.Start) // 左对齐
.justifyContent(FlexAlign.Start) // 顶部排列
这两者组合,确保了所有子组件从左上角原点开始依次排列。这是最符合用户阅读习惯的布局方式,也是绝大多数信息型页面的默认布局方案。
17.2 何时使用 ColumnStart?
- ✅ 消息列表:每条消息垂直排列,左对齐展示
- ✅ 注册/登录表单:表单项从上到下依次排列
- ✅ 设置页面:设置项分组纵向排列
- ✅ 文章/资讯列表:标题+摘要垂直排列
- ✅ 个人主页:头像+昵称+简介垂直堆叠
- ✅ 评论区:每条评论垂直排列
17.3 何时不适合 ColumnStart?
- ❌ 水平导航栏:应使用
Row - ❌ 网格/瀑布流:应使用
Grid/WaterFlow - ❌ 居中弹窗:应使用
Column+.alignItems(ItemAlign.Center) - ❌ 底部操作栏:应使用
Row+.justifyContent(FlexAlign.SpaceEvenly)
17.4 布局性能优化建议
- 减少嵌套层级:过深的 Column 嵌套会增加布局计算开销,尽量控制在 3 层以内
- 合理使用 ForEach:长列表提供 key 生成器,避免全量重建
- Scroll 与 Column 搭配:内容超出屏幕时,Scroll 是必要的;内容不足时,去掉 Scroll 可减少组件开销
- 避免不必要的 @State:只需要渲染的数据才用
@State装饰,普通常量数据不需要响应式
17.5 最后的话
ColumnStart 布局看似简单,却是构建鸿蒙原生应用 UI 的基石。掌握了 Column + alignItems(ItemAlign.Start) + justifyContent(FlexAlign.Start) 的组合,再配合 Row 的水平排列能力,你已经能够搭建出 90% 以上常见的移动端页面布局。
实际项目开发中,建议从最外层的 Column 开始规划页面的纵向结构,然后逐步在各个内容区块内部使用 Row 或嵌套 Column 来实现更精细的布局。始终记住 ArkUI 的布局哲学:容器决定排列方向,对齐属性控制具体位置,状态驱动 UI 更新。
祝愿你在 HarmonyOS NEXT 的开发旅程中,用 ColumnStart 布局构建出优雅高效的用户界面。
更多推荐



所有评论(0)