【共创季稿事节】鸿蒙原生 ArkTS 布局实战:Row 与 Column 混合嵌套实现经典表单布局
鸿蒙原生 ArkTS 布局实战:Row 与 Column 混合嵌套实现经典表单布局
一、引言
在移动端应用开发中,表单页面 是最常见、最高频的 UI 场景之一。无论是登录注册、用户信息编辑、设置页,还是订单提交,几乎每一个 App 都离不开表单。表单布局的核心诉求是:标签对齐、输入区域自适应、视觉节奏统一。
HarmonyOS NEXT 的 ArkUI 框架提供了 Row(水平容器)和 Column(垂直容器)两个基础布局组件。通过它们的混合嵌套,我们可以非常优雅地实现经典的表单布局 —— 这也是 ArkTS 声明式 UI 体系中最具代表性的布局模式之一。
本文将从一个完整的可运行示例出发,逐行剖析 Row + Column 混合嵌套 的实现思路、代码细节和布局最佳实践,帮助你快速掌握这一核心布局技能。
二、Row 与 Column 布局基础
2.1 Column —— 垂直排列容器
Column 是一个沿垂直方向(从上到下)排列子组件的容器。它的核心属性:
| 属性 | 作用 | 典型值 |
|---|---|---|
.justifyContent() |
主轴(垂直)方向的对齐方式 | FlexAlign.Start / Center / SpaceBetween |
.alignItems() |
交叉轴(水平)方向的对齐方式 | ItemAlign.Start / Center / Stretch |
.width() / .height() |
容器尺寸 | '100%' 或具体 vp 值 |
2.2 Row —— 水平排列容器
Row 是一个沿水平方向(从左到右)排列子组件的容器。它与 Column 共享同一套 Flexbox 布局模型,只是主轴方向不同:
| 属性 | 作用 | 典型值 |
|---|---|---|
.justifyContent() |
主轴(水平)方向的对齐方式 | FlexAlign.Start / Center / SpaceBetween |
.alignItems() |
交叉轴(垂直)方向的对齐方式 | ItemAlign.Center / Stretch |
.layoutWeight() |
子组件占用剩余空间的权重比例 | 正整数 |
2.3 混合嵌套的核心理念
将 Column 作为页面的「骨架」,把每一行表单条目作为 Row 嵌入其中,就构成了经典的表单布局结构:
Column(整体表单)
├── Row ① ── 标签 + 输入框
├── Row ② ── 标签 + 下拉选择
├── Row ③ ── 标签 + 日期选择
├── Row ④ ── 标签 + 输入框
└── Row ⑤ ── 提交按钮(居中)
这种结构清晰、可维护性强,且天然兼容不同屏幕宽度。
三、示例应用概览
3.1 功能说明
我们实现一个 用户信息登记表单,包含以下字段:
| 字段 | 组件类型 | 说明 |
|---|---|---|
| 用户名 | TextInput | 普通文本输入 |
| 密码 | TextInput(Password 模式) | 密码遮罩显示 |
| 性别 | Select(下拉选择器) | 四个选项:请选择/男/女/其他 |
| 出生日期 | DatePicker | 日期选择器 |
| 电子邮箱 | TextInput(Email 模式) | 邮箱格式输入 |
| 提交按钮 | Button | 居中展示,点击弹 Toast 提示 |
3.2 项目结构
entry/src/main/ets/pages/
├── Index.ets # 首页:提供导航入口
└── FormLayout.ets # 表单布局演示页面
3.3 路由注册
main_pages.json 中需要注册两个页面:
{
"src": [
"pages/Index",
"pages/FormLayout"
]
}
四、完整代码实现
4.1 首页 —— Index.ets
首页使用 RelativeContainer 居中显示标题文字,并在底部放置一个导航按钮,通过 router.pushUrl 跳转到表单演示页。
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
Button('打开表单布局演示 →')
.id('FormLayoutBtn')
.width(220)
.height(44)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#5B6ABF')
.borderRadius(22)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.margin({ bottom: 80 })
.onClick(() => {
router.pushUrl({ url: 'pages/FormLayout' });
})
}
.height('100%')
.width('100%')
}
}
4.2 核心 —— FormLayout.ets
这是本文的主角。完整代码如下(含详细注释):
/**
* FormLayout.ets —— Row + Column 混合嵌套:经典表单布局
*
* 布局要点:
* 1. 最外层用 Column 作为整个表单的垂直容器,自上而下排列每一行。
* 2. 每一「标签 + 输入框」是一行 Row,水平排列左侧标签和右侧输入区。
* 3. Row 内部的标签部分(Text)定宽,输入部分使用 .layoutWeight(1)
* 自适应剩余宽度,达到等宽对齐的经典表单效果。
* 4. 每行之间通过 .margin / .padding 控制间距,形成整洁的视觉节奏。
* 5. 底部 Button 独占一行,居中展示,作为表单提交入口。
*/
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct FormLayout {
// —— 表单数据状态 ——
@State userName: string = '';
@State password: string = '';
@State selectedGender: number = 0;
@State birthDate: number = Date.now();
@State email: string = '';
private genderOptions: SelectOption[] = [
{ value: '请选择' },
{ value: '男' },
{ value: '女' },
{ value: '其他' }
];
build() {
Scroll() {
Column() {
// 表单标题
Text('用户信息登记')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.margin({ top: 24, bottom: 28 })
.width('100%')
.textAlign(TextAlign.Center)
// Row ①:用户名
Row() {
Text('用户名')
.fontSize(16).fontColor('#333333')
.width(80).textAlign(TextAlign.End)
TextInput({ placeholder: '请输入用户名', text: this.userName })
.layoutWeight(1)
.height(40).fontSize(15)
.borderRadius(8).backgroundColor('#F5F5F5')
.onChange((v: string) => { this.userName = v; })
}
.width('100%')
.padding({ left: 16, right: 40 })
.margin({ bottom: 16 })
// Row ②:密码
Row() {
Text('密 码')
.fontSize(16).fontColor('#333333')
.width(80).textAlign(TextAlign.End)
TextInput({ placeholder: '请输入密码', text: this.password })
.layoutWeight(1).height(40).fontSize(15)
.type(InputType.Password)
.borderRadius(8).backgroundColor('#F5F5F5')
.onChange((v: string) => { this.password = v; })
}
.width('100%')
.padding({ left: 16, right: 40 })
.margin({ bottom: 16 })
// Row ③:性别
Row() {
Text('性 别')
.fontSize(16).fontColor('#333333')
.width(80).textAlign(TextAlign.End)
Select(this.genderOptions)
.selected(this.selectedGender)
.value(this.genderOptions[this.selectedGender].value)
.layoutWeight(1).height(40)
.borderRadius(8).backgroundColor('#F5F5F5')
.onSelect((i: number) => { this.selectedGender = i; })
}
.width('100%')
.padding({ left: 16, right: 40 })
.margin({ bottom: 16 })
// Row ④:出生日期
Row() {
Text('出生日期')
.fontSize(16).fontColor('#333333')
.width(80).textAlign(TextAlign.End)
DatePicker({
start: new Date('1900-01-01'),
end: new Date(Date.now()),
selected: new Date(this.birthDate)
})
.layoutWeight(1).height(40)
.onChange((value: DatePickerResult) => {
if (value.year && value.month && value.day) {
this.birthDate = new Date(
value.year, value.month - 1, value.day
).getTime();
}
})
}
.width('100%')
.padding({ left: 16, right: 40 })
.margin({ bottom: 16 })
// Row ⑤:电子邮箱
Row() {
Text('电子邮箱')
.fontSize(16).fontColor('#333333')
.width(80).textAlign(TextAlign.End)
TextInput({ placeholder: 'example@mail.com', text: this.email })
.layoutWeight(1).height(40).fontSize(15)
.type(InputType.Email)
.borderRadius(8).backgroundColor('#F5F5F5')
.onChange((v: string) => { this.email = v; })
}
.width('100%')
.padding({ left: 16, right: 40 })
.margin({ bottom: 28 })
// 提交按钮行
Row() {
Button('提 交')
.width(200).height(44)
.fontSize(18).fontWeight(FontWeight.Medium)
.backgroundColor('#5B6ABF').borderRadius(22)
.onClick(() => {
promptAction.showToast({
message: '表单已提交(演示布局,未实际发送)',
duration: 2000
});
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 32 })
// 布局说明脚注
Text('布局结构:Column 包裹多行 Row,'
+ '每行 Row 内「左侧标签定宽 + 右侧控件 layoutWeight(1) 自适应」')
.fontSize(12).fontColor('#999999')
.textAlign(TextAlign.Center)
.width('100%')
.padding({ left: 16, right: 16, bottom: 24 })
}
.width('100%')
.padding({ left: 12, right: 12 })
}
.height('100%')
.backgroundColor('#FFFFFF')
}
}
五、布局要点深度解析
5.1 Column 作为整体骨架
整个表单的最外层是一个 Column 组件。它的职责很简单:从上到下依次排列每一行表单条目。
Scroll() {
Column() {
// ... 所有 Row 在这里按顺序排列
}
}
为什么要用 Scroll 包裹?因为当表单字段较多或键盘弹出时,内容可能超出屏幕高度。Scroll 保证了良好的滚动体验。
5.2 Row 实现「标签 + 控件」水平布局
每一行表单条目都是一个 Row。Row 内部包含两个子组件:
- 左侧:
Text组件作为标签,固定宽度80vp - 右侧:输入控件(TextInput / Select / DatePicker)通过
.layoutWeight(1)自动填满剩余空间
这是整个布局模式最核心的技巧:
Row() {
Text('用户名').width(80).textAlign(TextAlign.End)
TextInput({ placeholder: '...' }).layoutWeight(1)
}
为什么是 .layoutWeight(1) 而不是百分比宽度?
.layoutWeight() 是 ArkUI Flexbox 布局中的权重分配机制。它让子组件按比例瓜分父容器的剩余空间,而不是基于父容器总宽度的百分比。这样做的好处:
- 标签宽度变化时自动适应:如果某个标签需要更宽(如 100vp),右侧输入框会自动收缩,无需手动调整百分比
- 容器宽度变化时自动适应:在折叠屏、平板等不同屏幕尺寸上,输入区域自动缩放
- 多个权重组件共存时灵活分配:如果 Row 内有两个
.layoutWeight(1)的组件,它们会均分剩余空间
相比之下,使用百分比宽度(如 width('70%'))需要手动计算并保持所有行一致,维护成本高。
5.3 标签右对齐的视觉考量
每个标签的 Text 组件都设置了 .textAlign(TextAlign.End),即文字右对齐。这样做的好处是:所有标签的右侧边缘对齐到同一垂直线,视觉上紧贴输入框,形成专业、整洁的表单外观。
用户名 │ [输入框]
密 码 │ [输入框]
性 别 │ [下拉选择器]
如果标签左对齐,不同长度的标签会导致输入框起始位置参差不齐,视觉上显得凌乱。
5.4 行间距与留白
每行 Row 通过 .margin({ bottom: 16 }) 设置 16vp 的下边距,形成均匀的行间距。同时 Row 本身有 .padding({ left: 16, right: 40 }),保证内容不紧贴屏幕边缘,右侧留白略大以保持视觉平衡。
5.5 多种输入组件的统一布局
本示例展示了四种不同的输入组件,它们在 Row 中的布局方式完全一致:
| 组件类型 | 数据绑定方式 | 特殊属性 |
|---|---|---|
| TextInput(普通) | text + onChange |
— |
| TextInput(密码) | text + onChange |
.type(InputType.Password) |
| Select | selected + value + onSelect |
数据源为 SelectOption[] |
| DatePicker | selected + onChange |
需转换 DatePickerResult 为时间戳 |
这种统一性意味着:一旦掌握了这个布局模式,你可以轻松组合任何 ArkUI 输入组件。
5.6 提交按钮居中
提交按钮使用一个独立的 Row 包裹,并设置 .justifyContent(FlexAlign.Center) 实现居中:
Row() {
Button('提 交') { ... }
}
.width('100%')
.justifyContent(FlexAlign.Center)
这里 Row 的宽度为 100%,Button 宽度固定为 200vp,通过主轴居中对齐使按钮位于屏幕中央。
六、布局性能与最佳实践
6.1 避免过度嵌套
虽然 Row + Column 嵌套是推荐的做法,但也要避免不必要的嵌套层级。例如,下面的结构就是多余的:
Column → Row → Column → Row → Text + TextInput ❌ 过度嵌套
正确的做法是:每一层嵌套都有明确的布局目的。表单场景下,两层嵌套(Column → Row)就足够了。
6.2 使用 .layoutWeight 而非固定宽度
对于输入区域,始终优先使用 .layoutWeight(1) 而非固定的 width 值。前者提供弹性布局,后者在屏幕尺寸变化时可能出现溢出或留白过多的问题。
6.3 标签宽度的一致性
所有标签的 width 值应保持一致(本示例统一为 80vp)。如果某个标签文字较长,可以考虑:
- 缩写或换行:减少文字长度
- 增大统一宽度:将所有标签宽度增加到 100vp 或 120vp
- 使用图标代替文字:对图标加 Tooltip 说明
不建议为不同行单独设置不同的标签宽度 —— 那会破坏对齐美感。
6.4 配合 Scroll 处理键盘弹起
当 TextInput 获得焦点时,系统键盘会弹出并遮挡部分表单内容。用 Scroll 包裹整个表单后,内容会自动向上滚动,确保正在输入的字段可见。这是移动端表单的必备处理。
6.5 状态管理与数据流
本示例使用 @State 装饰器管理表单数据。@State 是 ArkTS 中最基础的状态管理装饰器,它标记的变量变化时会自动触发 UI 重新渲染。
对于更复杂的表单(含校验、联动、异步提交),建议结合 @Observed / @ObjectLink 或 LocalStorage / AppStorage 进行结构化状态管理。
七、扩展与变体
7.1 多列布局
如果表单字段较多,可以将 Row 内的「标签 + 控件」扩展为多列:
Row() {
// 第一组
Column() {
Text('姓名').width(60)
TextInput()
}.layoutWeight(1)
// 第二组
Column() {
Text('年龄').width(60)
TextInput()
}.layoutWeight(1)
}
.width('100%')
7.2 带校验的表单
在 Row 下方增加一个用于显示错误信息的 Text 组件:
Column() {
Row() {
Text('用户名').width(80)
TextInput({ ... }).layoutWeight(1)
}
// 错误提示
Text('用户名不能为空')
.fontSize(12).fontColor('#FF3333')
.width('100%')
.padding({ left: 80 }) // 与输入框对齐
}
.margin({ bottom: 16 })
7.3 分组表单
对多个字段进行分组,每组有标题栏:
Column() {
// 分组标题
Text('基本信息')
.fontSize(18).fontWeight(FontWeight.Bold)
.width('100%').margin({ top: 16, bottom: 12 })
// 组内字段
Row() { /* 姓名 */ }
Row() { /* 性别 */ }
Row() { /* 出生日期 */ }
// 另一组
Text('联系方式')
.fontSize(18).fontWeight(FontWeight.Bold)
.width('100%').margin({ top: 24, bottom: 12 })
Row() { /* 邮箱 */ }
Row() { /* 电话 */ }
}
八、总结
本文通过一个完整的「用户信息登记表单」示例,详细讲解了鸿蒙 NEXT 原生 ArkTS 中 Row + Column 混合嵌套 实现经典表单布局的全过程。
核心要点回顾
| 要点 | 实现方式 | 说明 |
|---|---|---|
| 整体骨架 | Column | 垂直排列所有行 |
| 每行条目 | Row | 水平排列「标签 + 输入区」 |
| 标签对齐 | Text.width(80) + textAlign(End) |
定宽 + 右对齐,形成统一垂直线 |
| 输入区自适应 | .layoutWeight(1) |
弹性占用剩余宽度,适配不同屏幕 |
| 行间距 | .margin({ bottom: 16 }) |
均匀的 16vp 间距 |
| 滚动支持 | Scroll 包裹 | 防止键盘弹起遮挡内容 |
| 按钮居中 | Row + justifyContent(Center) |
独立行居中展示 |
适用场景
- 用户注册 / 登录页
- 个人资料编辑
- 设置页面
- 订单填写 / 收货地址
- 反馈与问卷
- 任何需要「标签 + 控件」成对出现的页面
Row 与 Column 的混合嵌套是 ArkTS 声明式 UI 中最基础也最强大的布局模式。掌握了它,你就掌握了 HarmonyOS NEXT 应用开发的布局核心。
本文配套的完整示例代码位于项目 entry/src/main/ets/pages/FormLayout.ets,可在 DevEco Studio 中直接运行查看效果。


更多推荐


所有评论(0)