【共创季稿事节】鸿蒙ArkTS布局实战——使用Scroll+Column+Row构建优雅的可滚动表格

鸿蒙 ArkTS 布局实战------使用 Scroll + Column + Row 构建优雅的可滚动表格


一、前言

在移动端和桌面端开发中,表格 是最常见的数据展示形式。无论是员工信息表、财务报表还是任务看板,表格都不可或缺。HarmonyOS ArkUI 没有提供一个开箱即用的 Table 组件,但通过组合 Scroll(滚动容器)、Column(纵向布局)和 Row(横向布局)三个基础组件,我们可以在 200 行代码之内构建出功能完整的可滚动数据表格。

本文基于真实项目 ap15,逐行解读如何利用 Scroll + Column + Row 布局组合构建一个包含标题栏、表头、数据行、行选中交互、底部统计栏的完整表格页面,API 版本 24 目标构建。


二、项目结构概览

复制代码
ap15/
├── AppScope/app.json5           # 应用级配置(包名、版本等)
├── entry/src/main/ets/
│   ├── entryability/
│   │   └── EntryAbility.ets     # Ability 生命周期
│   └── pages/
│       ├── Index.ets            # 主页(导航入口)
│       └── ScrollTableDemo.ets  # 表格演示页(核心,261 行)
└── build-profile.json5          # 构建配置(stageMode,API 24)

项目采用 Stage 模型apiType: "stageMode"),两个核心页面职责清晰:Index.ets 做导航,ScrollTableDemo.ets 做功能。


三、Ability 生命周期与路由导航

3.1 EntryAbility 生命周期

typescript 复制代码
export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    this.context.getApplicationContext()
      .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
  }
  onWindowStageCreate(windowStage) {
    windowStage.loadContent('pages/Index', (err) => { /* ... */ });
  }
}

setColorMode(COLOR_MODE_NOT_SET) 让应用跟随系统深色/浅色主题,这是值得推荐的开发习惯。

3.2 首页导航页

typescript 复制代码
@Entry @Component
struct Index {
  build() {
    Column() {
      Text('鸿蒙 ArkTS 布局示例')
        .fontSize(26).fontWeight(FontWeight.Bold)
        .fontColor('#1A1A2E').margin({ top: 80, bottom: 20 })
      Text('Scroll + Column + Row\n构建可滚动表格')
        .fontSize(16).fontColor('#666666')
        .textAlign(TextAlign.Center).margin({ bottom: 60 })
      Button() {
        Row() {
          Text('📋').fontSize(22).margin({ right: 8 })
          Text('进入表格演示').fontSize(18).fontWeight(FontWeight.Medium)
        }
      }
      .width(240).height(54)
      .backgroundColor('#3A7BD5').borderRadius(27)
      .shadow({ radius: 8, color: 'rgba(58,123,213,0.3)', offsetY: 4 })
      .onClick(() => router.pushUrl({ url: 'pages/ScrollTableDemo' }))
    }
    .width('100%').height('100%').backgroundColor('#F0F2F5')
  }
}

首页设计干净利落:灰色背景 + 蓝色主按钮 + 清晰的说明文字,引导用户进入演示页面。router.pushUrl 是页面级导航的标准方式。


四、核心布局------Scroll + Column + Row 三层架构

4.1 布局层级全景图

复制代码
Stack                          ← 最外层,居中
  └── Column                   ← 主容器(标题 + 表格卡片)
        ├── Text               ← 页面标题
        └── Column             ← 表格容器(白底 + 圆角 + 阴影)
              ├── Row (表头)   ← 蓝色背景,5 列表头
              ├── Scroll       ← 【核心】可滚动区域,占 70% 高度
              │     └── Column ← 纵向排列数据行
              │           ├── Row → (数据行 1)
              │           ├── Row → (数据行 2)
              │           └── Row → (数据行 15)
              └── Row (底部栏) ← 统计信息 + 选中状态

这个架构的精髓:每一层职责单一,Row 既做表头又做数据行,Scroll 提供滚动能力,Column 做纵向排列。

层级 组件 职责
1 Stack 全屏布局基准
2 Column 垂直排列标题和表格
3 Column 表格卡片(白底、圆角、阴影)
4 Row 表头行(蓝色强调)
5 Scroll 提供垂直滚动
6 Column 数据行竖向排列
7 Row 单行数据展示(5 列)
8 Row 底部统计栏

4.2 数据模型与状态管理

typescript 复制代码
class Employee {
  id: string; name: string; department: string;
  position: string; entryDate: string;
  constructor(id, name, department, position, entryDate) {
    this.id = id; this.name = name; this.department = department;
    this.position = position; this.entryDate = entryDate;
  }
}

选择 class 而非 interface 的原因:class 有构造函数、可扩展方法、调试信息更丰富。

@State 状态装饰器

typescript 复制代码
@State tableTitle: string = '员工信息一览表';
@State columns: string[] = ['工号', '姓名', '部门', '职位', '入职日期'];
@State columnWeights: number[] = [1, 1, 1.5, 1.5, 1.2];
@State employees: Employee[] = [ /* 15 条数据 */ ];
@State selectedIndex: number = -1;

@State 的工作机制:

  • 变量变化时 自动触发关联 UI 重新渲染
  • 渲染是 增量式 的,只刷新受影响的部分
  • 数组的变更方法(push、splice 等)同样触发更新

五、表格渲染核心代码精读

5.1 灵活分配列宽------layoutWeight

typescript 复制代码
@State columnWeights: number[] = [1, 1, 1.5, 1.5, 1.2];

// 表头和数据行统一使用
Text(col)
  .layoutWeight(this.columnWeights[index])

layoutWeight 类似 CSS Flexbox 的 flex-grow:同一 Row 下的子组件按权重比例瓜分容器宽度。

权重设计逻辑:

  • 工号(1)------ 短文本,占用最小
  • 姓名(1)------ 同工号
  • 部门(1.5)------ 如"技术研发部"需要更多空间
  • 职位(1.5)------ 同上
  • 入职日期(1.2)------ 日期格式固定

这样表格可自动适配不同屏幕尺寸,无需任何像素级硬编码。

5.2 表头渲染

typescript 复制代码
Row() {
  ForEach(this.columns, (col: string, index: number) => {
    Text(col)
      .fontSize(14).fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF').textAlign(TextAlign.Center)
      .layoutWeight(this.columnWeights[index])
      .padding({ top: 12, bottom: 12 })
  })
}
.width('100%').backgroundColor('#3A7BD5')
.borderRadius({ topLeft: 8, topRight: 8 })

设计亮点:

  • 动态列生成ForEach 遍历 columns 数组,列数变更时 UI 自动同步
  • 弹性宽度对齐 :表头和数据行使用相同的 columnWeights,完美对齐
  • 视觉风格:深蓝底 + 白字,上圆角与数据行直角过渡
  • 表头在 Scroll 外部 ,实现了 固定表头 + 滚动数据体 的经典 UI 模式

5.3 数据行------Scroll + Column + Row 三剑客

typescript 复制代码
Scroll() {
  Column() {
    ForEach(this.employees, (item: Employee, index: number) => {
      Row() {
        Text(item.id)     .fontSize(13).fontColor('#333333')
          .textAlign(TextAlign.Center).layoutWeight(this.columnWeights[0])
        Text(item.name)   .fontSize(13).fontColor('#333333')
          .textAlign(TextAlign.Center).layoutWeight(this.columnWeights[1])
        Text(item.department).fontSize(13).fontColor('#333333')
          .textAlign(TextAlign.Center).layoutWeight(this.columnWeights[2])
        Text(item.position).fontSize(13).fontColor('#333333')
          .textAlign(TextAlign.Center).layoutWeight(this.columnWeights[3])
        Text(item.entryDate).fontSize(13).fontColor('#333333')
          .textAlign(TextAlign.Center).layoutWeight(this.columnWeights[4])
      }
      .width('100%').height(48)
      .padding({ left: 4, right: 4 })
      .onClick(() => {
        this.selectedIndex = (this.selectedIndex === index) ? -1 : index;
        promptAction.showToast({
          message: `选中: ${item.name} (${item.id})`, duration: 1500
        });
      })
      .border({ width: { bottom: 1 }, color: { bottom: '#EAECF0' } })
      .backgroundColor(index === this.selectedIndex ? '#EBF5FF' : '#FFFFFF')
    })
  }.width('100%')
}
.width('100%').height('70%')
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Vertical)
.clip(true)

三层嵌套逐层拆解

第一层:Scroll------滚动能力的提供者

  • .height('70%'):限定滚动区域,余下 30% 留给底部栏
  • .edgeEffect(EdgeEffect.Spring):边缘弹簧回弹,提升手感
  • .scrollBar(BarState.Auto):仅滚动时显示滚动条
  • .scrollable(ScrollDirection.Vertical):仅纵向滚动
  • .clip(true):裁剪超出部分,配合圆角

第二层:Column------数据行的纵向容器

  • 不设固定 height,由子节点(15行×48px)撑开
  • 总高度超过 Scroll 视口时自动激活滚动

第三层:Row------单行数据展示

  • 固定 48px 行高,视觉整齐
  • 点击切换选中状态,Toast 即时反馈
  • 底部边框实现表格分隔线
  • 条件背景色:选中行浅蓝,未选中白色

5.4 底部统计栏

typescript 复制代码
Row() {
  Text(`共 ${this.employees.length} 条记录`)
    .fontSize(13).fontColor('#888888')
    .textAlign(TextAlign.Start).layoutWeight(1)
  Text(this.selectedIndex >= 0
    ? `当前选中: ${this.employees[this.selectedIndex].name}`
    : '点击行可选中')
    .fontSize(13)
    .fontColor(this.selectedIndex >= 0 ? '#3A7BD5' : '#AAAAAA')
    .textAlign(TextAlign.End).layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10, left: 8, right: 8 })
.backgroundColor('#F8F9FC')
.borderRadius({ bottomLeft: 8, bottomRight: 8 })

交互细节:左侧固定显示总记录数,右侧动态变化(未选中 → 灰色提示"点击行可选中";选中后 → 蓝色显示选中姓名)。下圆角与表头上圆角呼应,形成完整卡片。

5.5 表格卡片容器

typescript 复制代码
Column() { /* 表头 + Scroll + 底部栏 */ }
.width('92%')
.backgroundColor('#FFFFFF').borderRadius(8)
.shadow({ radius: 12, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 4 })

width('92%') 两侧留 4% 边距,配合 Stack 居中,形成 悬浮卡片 效果。


六、交互机制深度解析

6.1 选中/取消选中逻辑

typescript 复制代码
this.selectedIndex = (this.selectedIndex === index) ? -1 : index;

这是一个 Toggle 模式

操作 之前 selectedIndex 之后 效果
点击未选中行 -1 index 选中 + 蓝底 + Toast
点击已选中行 index -1 取消选中,恢复白底
点击 A 再点 B A B 选中切换

6.2 状态驱动的 UI 更新

typescript 复制代码
.backgroundColor(index === this.selectedIndex ? '#EBF5FF' : '#FFFFFF')

"UI 是状态的函数"------ArkTS 的内联条件判断让视图与状态绑定,状态变化时 UI 自动增量刷新,无需手动操作 DOM。


七、最佳实践与可优化方向

7.1 代码中的优秀实践

实践 具体体现
数据视图分离 employees、columns 集中定义,与 build() 分离
弹性布局适配多屏 layoutWeight 弹性列宽,非固定像素
节制的视觉设计 仅 3 种主色:蓝(#3A7BD5)、灰系、黑系
详略得当的注释 每个 @State 都有说明,布局层级有 ASCII 图
可逆的交互设计 再次点击可取消选中,而非"一次性"操作

7.2 可优化的方向

① 添加横向滚动支持:如果列数超过 5 列,可嵌套双层 Scroll:

复制代码
Scroll(水平) → Column → Row(表头) + Scroll(垂直) → Column → Row×N

② 数据源动态化:从静态数据改为网络请求:

typescript 复制代码
aboutToAppear() { this.fetchEmployees(); }
async fetchEmployees() {
  this.employees = await http.request('https://api.example.com/employees');
}

③ 加载态与空状态:LoadingProgress 加载中组件 + "暂无数据"占位图。

④ 下拉刷新 :使用 Refresh 组件包裹表格内容。

⑤ @Builder 抽取重复 UI

typescript 复制代码
@Builder cellText(text: string, weight: number) {
  Text(text)
    .fontSize(13).fontColor('#333333')
    .textAlign(TextAlign.Center).layoutWeight(weight)
}

数据行代码从 22 行缩减到 7 行。


八、布局方案横向对比

方案 核心组件 优点 缺点 适用场景
Scroll+Column+Row Scroll, Column, Row 灵活、轻量 需手动对齐列宽 中等复杂度表格
List List, ListItem 高性能(懒加载) 不适合多列 单列列表、聊天
Grid Grid, GridItem 规整网格 列数固定 相册、卡片
Canvas Canvas 高度自定义 开发量大 图表、报表

Scroll+Column+Row 方案在灵活性和开发效率之间取得了最佳平衡,适合多数企业级信息展示场景。


九、鸿蒙 ArkTS 开发经验总结

9.1 声明式 UI 思维转变

描述"UI 应该是什么",而非"如何变成什么"。

你不再需要 findViewByIdsetTextsetOnClickListener 的指令链,而是直接声明界面结构,框架自动响应状态变化。

9.2 DevEco Studio 实用工具

  • Previewer:实时预览 UI,比反复编译运行到真机高效
  • ArkUI Inspector:调试时查看组件树和属性
  • Profile:分析布局性能、检测过度绘制

9.3 关于 API 版本

项目 build-profile.json5compatibleSdkVersion 决定了最低系统版本。API 24 环境下的 API 变更主要包括:

  • @ohos.* 模块路径统一为 @kit.*(如 @kit.ArkUI
  • 路由 API router.pushUrl 替代旧版 router.push
  • 新增 edgeEffect 弹簧回弹等体验增强属性

十、总结

10.1 核心要点

  1. 布局方案Scroll + Column + Row 三层嵌套,构建可滚动表格
  2. 数据驱动@State 装饰器实现响应式 UI
  3. 弹性布局layoutWeight 按比例分配列宽,适配多屏
  4. 交互设计:Toggle 选中/取消 + Toast 即时反馈
  5. 视觉规范:克制配色、圆角卡片、层次分明的阴影

10.2 对鸿蒙生态的思考

鸿蒙没有提供命名就叫 Table 的组件------这不是缺失,而是深思熟虑的设计。不同产品中的表格形态差异巨大(固定列、可滚动列、合并单元格、行内编辑),提供基础布局组件让开发者自由组合,远比提供一个"万能"的 Table 组件更灵活。

学习布局,就是学习组合。掌握组合,你就掌握了一切。

10.3 延伸阅读

  • 本文分析的完整源码:entry/src/main/ets/pages/Index.ets(60 行)与 ScrollTableDemo.ets(261 行)
  • 可直接将 ScrollTableDemo.ets 的代码复制为表格模板,只需替换数据模型和列定义即可
  • HarmonyOS API 24 参考:developer.harmonyos.com

本文基于 HarmonyOS SDK 6.1.0(API Version 24)、DevEco Studio 编写。代码已在真机验证。