鸿蒙原生 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 布局中的权重分配机制。它让子组件按比例瓜分父容器的剩余空间,而不是基于父容器总宽度的百分比。这样做的好处:

  1. 标签宽度变化时自动适应:如果某个标签需要更宽(如 100vp),右侧输入框会自动收缩,无需手动调整百分比
  2. 容器宽度变化时自动适应:在折叠屏、平板等不同屏幕尺寸上,输入区域自动缩放
  3. 多个权重组件共存时灵活分配:如果 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)。如果某个标签文字较长,可以考虑:

  1. 缩写或换行:减少文字长度
  2. 增大统一宽度:将所有标签宽度增加到 100vp 或 120vp
  3. 使用图标代替文字:对图标加 Tooltip 说明

不建议为不同行单独设置不同的标签宽度 —— 那会破坏对齐美感。

6.4 配合 Scroll 处理键盘弹起

当 TextInput 获得焦点时,系统键盘会弹出并遮挡部分表单内容。用 Scroll 包裹整个表单后,内容会自动向上滚动,确保正在输入的字段可见。这是移动端表单的必备处理。

6.5 状态管理与数据流

本示例使用 @State 装饰器管理表单数据。@State 是 ArkTS 中最基础的状态管理装饰器,它标记的变量变化时会自动触发 UI 重新渲染。

对于更复杂的表单(含校验、联动、异步提交),建议结合 @Observed / @ObjectLinkLocalStorage / 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 中直接运行查看效果。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

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

更多推荐