复选框是多选交互的基础。待办事项的勾选完成、批量选择后的操作、权限设置中的多项启用——所有这些场景都需要复选框来承载。与 Radio 不同,Checkbox 允许用户在同一组中选择零个、一个或多个选项,互不排斥。

HarmonyOS NEXT ArkUI 提供了 Checkbox 组件——一个标准的复选框控件,配合 group 属性和 select() 方法实现组管理和选中状态控制。本文将深入讲解 Checkbox 组件的 API 与使用技巧,并构建一个"任务清单"——支持任务添加、勾选切换、筛选(全部/未完成/已完成)、清除已完成和完成进度统计。

关键词:HarmonyOS、ArkUI、Checkbox、复选框、任务清单、状态筛选、批量操作

一、Checkbox 组件 API

1.1 基本用法

Checkbox({ name: 'task1', group: 'tasks' })
  .select(true)
  .width(22)
  .height(22)
  .onChange((value: boolean) => {
    // value: true 表示选中,false 表示取消选中
    this.toggleTask(1);
  })

核心参数与属性:

参数/属性 类型 说明
name string 该 Checkbox 的唯一标识
group string 组名,同一组内的 Checkbox 可统一管理
.select() boolean 设置选中状态(true 选中 / false 未选中)
.width() / .height() Length 复选框的尺寸
.onChange() callback 选中状态变化时的回调,参数为 boolean

1.2 select 状态控制

Checkbox 使用 .select() 方法控制选中状态,而非 Radio 的 .checked()。这是一个容易混淆的 API 差异:

// Radio 使用 .checked()
Radio({ value: '0', group: 'q1' })
  .checked(this.selected === 0)

// Checkbox 使用 .select()
Checkbox({ name: '1', group: 'tasks' })
  .select(this.tasks.find(t => t.id === 1).done)

两者的语义不同:Radio 的"checked"暗示互斥(同一时刻只有一个被选中),Checkbox 的"select"暗示勾选(可多选、互不排斥)。

Demo 中将每个任务的 done 状态绑定到对应 Checkbox 的 .select()

Checkbox({ name: task.id.toString(), group: 'tasks' })
  .select(task.done)
  .onChange((value: boolean) => {
    this.toggleTask(task.id);
  })

1.3 group 分组管理

与 Radio 类似,Checkbox 也支持 group 属性。同一 group 的 Checkbox 属于同一个逻辑组,方便后续扩展批量操作(全选、全不选、反选等)。Demo 中所有任务的 Checkbox 使用统一组名 'tasks'

Checkbox({ name: task.id.toString(), group: 'tasks' })

虽然当前 Demo 没有实现"全选"按钮,但统一的分组为后续扩展留下了接口——只需遍历同组 Checkbox 即可实现全选/全不选。

1.4 onChange 回调

当用户点击 Checkbox 或同行区域触发选中变化时,onChange 回调被调用:

.onChange((value: boolean) => {
  this.toggleTask(task.id);
})

value 参数表示变化后的选中状态(truefalse)。Demo 中直接调用 toggleTask(id) 方法更新数据,而不用 value 参数——因为 toggleTask 本身就是取反操作,与 Checkbox 状态变化方向一致。

在某些场景下,你可能需要根据 value 做条件处理:

.onChange((value: boolean) => {
  if (value) {
    // 勾选时执行的操作
    this.markComplete(task.id);
  } else {
    // 取消勾选时执行的操作
    this.markIncomplete(task.id);
  }
})

在这里插入图片描述

二、任务清单的整体设计

2.1 页面架构

ChecklistPage
├── 标题栏 — "任务清单" + 完成进度(done/total)
├── 输入卡片(白色背景)
│   ├── 添加任务输入框 + "添加"按钮
│   └── 筛选标签(全部/未完成/已完成)+ 清除已完成按钮
└── 任务列表(可滚动)
    ├── Checkbox + 任务标题 + 时间
    ├── 行间分割线
    └── 空状态提示

2.2 数据类设计

Demo 中定义了两个类来承载数据:

class Stats {
  total: number;
  done: number;
  active: number;

  constructor(total: number, done: number, active: number) {
    this.total = total;
    this.done = done;
    this.active = active;
  }
}

class TaskItem {
  id: number;
  title: string;
  done: boolean;
  time: string;

  constructor(id: number, title: string, done: boolean, time: string) {
    this.id = id;
    this.title = title;
    this.done = done;
    this.time = time;
  }

  toggleDone(): TaskItem {
    return new TaskItem(this.id, this.title, !this.done, this.time);
  }
}

关键设计考量:

Stats 类:ArkTS 不允许将对象字面量作为返回值类型(arkts-no-obj-literals-as-types),所以统计信息必须用显式的类来承载。Stats 包含 totaldoneactive 三个字段,分别表示任务总数、已完成数和未完成数。

TaskItem 类toggleDone() 方法返回一个新的 TaskItem 实例,而非修改原实例的 done 字段。这是 ArkTS 状态管理的要求——@State 数组的变化必须通过创建新数组来触发 UI 刷新,而数组中每个元素也应该是新的引用。

2.3 预置任务

@State tasks: TaskItem[] = this.getPresetTasks();

getPresetTasks(): TaskItem[] {
  return [
    new TaskItem(1, '购买牛奶和面包', true, '今天 08:30'),
    new TaskItem(2, '完成项目进度报告', false, '今天 14:00'),
    new TaskItem(3, '预约牙科检查', false, '明天 10:00'),
    new TaskItem(4, '阅读《ArkUI 开发指南》第 5 章', true, '昨天'),
    new TaskItem(5, '整理桌面文件', false, '今天'),
    new TaskItem(6, '给客户回复邮件', false, '今天 16:00'),
    new TaskItem(7, '缴纳水电费', true, '昨天'),
  ];
}

7 个预置任务覆盖了不同状态(3 个已完成、4 个未完成)和不同时间跨度(今天、明天、昨天),让筛选功能可以立即看到效果。
在这里插入图片描述

三、筛选机制

3.1 筛选状态管理

@State filter: string = 'all'; // 'all' | 'active' | 'done'

单选筛选——使用一个 @State 字符串控制当前显示的任务集合。三个值对应三个筛选标签:

filter 值 含义 显示内容
'all' 全部 所有任务
'active' 未完成 done === false 的任务
'done' 已完成 done === true 的任务

3.2 筛选逻辑

getFilteredTasks(): TaskItem[] {
  if (this.filter === 'active') return this.tasks.slice().filter((t: TaskItem) => !t.done);
  if (this.filter === 'done') return this.tasks.slice().filter((t: TaskItem) => t.done);
  return this.tasks.slice();
}

每当 filtertasks 变化时,getFilteredTasks() 会重新计算,返回过滤后的任务列表。注意这里使用 this.tasks.slice() 而非直接 this.tasks——slice() 创建浅拷贝,确保筛选结果不影响原始数据。

3.3 筛选标签 UI

三个筛选标签使用 @Builder 方法构建,实现代码复用:

@Builder
filterTab(label: string, key: string) {
  Text(label)
    .fontSize(13)
    .fontColor(key === this.filter ? '#FFFFFF' : '#666677')
    .fontWeight(key === this.filter ? FontWeight.Bold : FontWeight.Normal)
    .padding({ top: 6, bottom: 6, left: 14, right: 14 })
    .borderRadius(16)
    .backgroundColor(key === this.filter ? '#1677FF' : '#F2F3F5')
    .margin({ right: 8 })
    .onClick(() => { this.filter = key; })
}

选中标签:蓝底白字加粗(#1677FF 背景 / #FFFFFF 文字 / FontWeight.Bold),未选中标签:灰底灰字正常粗细(#F2F3F5 背景 / #666677 文字)。

三个标签在 Row 中水平排列:

Row() {
  this.filterTab('全部', 'all')
  this.filterTab('未完成', 'active')
  this.filterTab('已完成', 'done')
  Blank()
  // 清除已完成按钮(条件显示)
}

Blank() 将"清除已完成"按钮推到右侧,与筛选标签形成左右分区的布局。

四、任务的增删改

4.1 添加任务

addTask(): void {
  let title = this.newTaskTitle.trim();
  if (title.length === 0) return;
  let now = new Date();
  let timeStr = '今天 '.concat(
    now.getHours().toString().padStart(2, '0')).concat(':').concat(
    now.getMinutes().toString().padStart(2, '0'));
  let task = new TaskItem(this.nextId, title, false, timeStr);
  this.nextId++;
  this.tasks = this.tasks.slice().concat(task);
  this.newTaskTitle = '';
  promptAction.showToast({ message: '已添加任务', duration: 1500 });
}

流程分五步:

  1. trim() 去除首尾空格,如果为空直接返回
  2. new Date() 获取当前时间,通过 .getHours().getMinutes() 格式化为 HH:MM 的时间字符串
  3. 创建新 TaskItem,状态为 done = false(未完成)
  4. 通过 slice().concat(task) 创建新数组并赋值给 @State tasks,触发 UI 刷新
  5. 清空输入框,显示 Toast 提示

nextId 是一个自增计数器(初始值 10,避免与预置任务的 1-7 冲突),每添加一个任务自增 1。

4.2 切换完成状态

toggleTask(id: number): void {
  this.tasks = this.tasks.slice().map((t: TaskItem) => {
    if (t.id === id) return t.toggleDone();
    return t;
  });
}

找到对应 ID 的任务,调用 toggleDone() 创建状态取反后的新实例,其他任务保持不变。slice() + map() 的组合确保整个数组是新引用,触发 ArkUI 的变更检测。

4.3 清除已完成任务

clearDone(): void {
  let doneCount = this.getStats().done;
  this.tasks = this.tasks.slice().filter((t: TaskItem) => !t.done);
  promptAction.showToast({
    message: '已清除 '.concat(doneCount.toString()).concat(' 项已完成'),
    duration: 1500
  });
}

过滤出所有 done === false 的任务作为新数组。Toast 中显示清除的具体数量,让用户感知操作结果。

注意"清除已完成"按钮只在有已完成任务时才显示:

if (this.getStats().done > 0) {
  Text('清除已完成')
    .fontSize(12)
    .fontColor('#FF4D4F')
    // ...
    .onClick(() => { this.clearDone(); })
}

这个条件渲染避免了在没有已完成任务时出现无操作意义的按钮。

4.4 任务行的视觉设计

每行任务包含 Checkbox + 标题和时间:

Row() {
  Checkbox({ name: task.id.toString(), group: 'tasks' })
    .select(task.done)
    .width(22).height(22)
    .onChange((value: boolean) => {
      this.toggleTask(task.id);
    })

  Column() {
    Text(task.title)
      .fontSize(15)
      .fontColor(task.done ? '#BBBBCC' : '#1a1a2e')
      .fontWeight(task.done ? FontWeight.Normal : FontWeight.Medium)
      .decoration({
        type: task.done ? TextDecorationType.LineThrough : TextDecorationType.None
      })
      .maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
    Text(task.time)
      .fontSize(11)
      .fontColor('#CCCCDD')
      .margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)
  .margin({ left: 12 })
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, top: 12, bottom: 12 })
.backgroundColor(task.done ? '#FAFCFF' : '#FFFFFF')

已完成的任务有四个视觉变化:

  1. 文字颜色变浅#BBBBCC vs #1a1a2e
  2. 字重变轻FontWeight.Normal vs FontWeight.Medium
  3. 删除线TextDecorationType.LineThrough
  4. 行背景微蓝#FAFCFF vs #FFFFFF

这四个变化共同形成一个明确的视觉信号——已完成和未完成任务一目了然。

五、空状态提示

当筛选结果为空时,显示差异化的空状态提示:

if (this.getFilteredTasks().length === 0) {
  Column() {
    Text(this.filter === 'done' ? '暂无已完成任务' :
      (this.filter === 'active' ? '所有任务都已完成' : '暂无任务'))
      .fontSize(14).fontColor('#BBBBCC')
    Text(this.filter === 'done' ? '完成任务后勾选即可' : '点击上方输入框添加任务')
      .fontSize(12).fontColor('#CCCCDD')
      .margin({ top: 4 })
  }
  .width('100%').padding({ top: 48, bottom: 48 })
}

三种空状态对应不同的提示文字:

filter 主文字 副文字
'all' 暂无任务 点击上方输入框添加任务
'active' 所有任务都已完成 点击上方输入框添加任务
'done' 暂无已完成任务 完成任务后勾选即可

空状态设计是移动端 UX 的重要细节——用户看到空白列表时需要知道原因和下一步该做什么,而不是面对一片空白感到困惑。

六、完成进度统计

标题栏右侧显示完成进度——这是任务管理类应用的核心信息:

Row() {
  Text('任务清单')
    .fontSize(FontSize.HEADLINE)
    .fontColor('#FFFFFF')
    .fontWeight(FontWeight.Bold)
  Blank()
  Column() {
    Text(this.getStats().done.toString().concat('/').concat(this.getStats().total.toString()))
      .fontSize(15)
      .fontColor('#FFFFFF')
      .fontWeight(FontWeight.Bold)
    Text('已完成')
      .fontSize(10)
      .fontColor('#FFFFFFCC')
  }
  .alignItems(HorizontalAlign.Center)
  .padding({ top: 6, bottom: 6, left: 12, right: 12 })
  .borderRadius(12)
  .backgroundColor('#FFFFFF20')
}

getStats() 实时计算任务总数和已完成数:

getStats(): Stats {
  let total = this.tasks.length;
  let done = this.tasks.filter((t: TaskItem) => t.done).length;
  return new Stats(total, done, total - done);
}

进度显示为半透明圆角卡片(#FFFFFF20 背景 + 12px 圆角),与深色标题栏(#1a1a2e)形成层次分明的视觉效果。

七、输入区域设计

输入区域位于标题栏下方,独立的白色背景卡片中:

Row() {
  TextInput({ text: this.newTaskTitle, placeholder: '添加新任务...' })
    .fontSize(14)
    .layoutWeight(1)
    .height(42)
    .backgroundColor('#FFFFFF00')
    .borderRadius(0)
    .onChange((value: string) => { this.newTaskTitle = value; })
    .onSubmit((enterKey: EnterKeyType) => { this.addTask(); })

  if (this.newTaskTitle.length > 0) {
    Text('添加')
      .fontSize(13)
      .fontColor('#FFFFFF')
      .fontWeight(FontWeight.Medium)
      .padding({ top: 6, bottom: 6, left: 14, right: 14 })
      .borderRadius(16)
      .backgroundColor('#1677FF')
      .margin({ left: 8 })
      .onClick(() => { this.addTask(); })
  }
}
.width('100%')
.height(42)
.borderRadius(21)
.backgroundColor('#F2F3F5')
.padding({ left: 16, right: 8 })

设计要点:

输入框:圆角 21px(正圆形 pill 风格),浅灰背景 #F2F3F5,文本输入区设为透明背景以融入容器。

条件显示的添加按钮:只有当 newTaskTitle 有内容时才显示"添加"按钮。空内容时隐藏按钮,用户可以通过键盘回车键(.onSubmit())提交——这避免了空按钮占据空间的尴尬。

双提交路径

  1. 点击"添加"按钮 → 触发 addTask()
  2. 键盘按回车键 → .onSubmit() 回调 → 触发 addTask()

这种双路径设计让用户可以根据习惯选择操作方式——触摸点击或键盘确认。

八、交互流程演示

8.1 查看任务

进入页面,7 个预置任务按添加顺序排列。标题栏显示"3/7 已完成"。已完成任务显示为浅灰色带删除线,未完成任务显示为深色正常文字。行间有分割线。

8.2 添加任务

点击输入框,输入"取快递"。输入框右侧出现"添加"按钮。点击"添加",Toast 提示"已添加任务",新任务出现在列表底部(未完成状态)。标题栏变为"3/8 已完成"。

8.3 勾选完成任务

点击第 2 个任务"完成项目进度报告"的复选框。Checkbox 变为选中状态,任务标题变为浅灰色带删除线,行背景变为微蓝色。标题栏变为"4/8 已完成"。

再点击同一个 Checkbox 取消完成。任务恢复到未完成状态,标题栏退回"3/8"。

8.4 筛选任务

点击"未完成"标签。列表只显示未完成的任务(5 条)。标题栏进度不变。

点击"已完成"标签。列表只显示已完成的任务(3 条)。标题栏进度不变。

点击"全部"标签。列表恢复显示所有任务。

8.5 清除已完成

确保当前有已完成任务后,点击右上角红色的"清除已完成"按钮。所有已完成任务被移除。Toast 提示清除的数量。标题栏变为"0/5",因为剩余任务都未完成。筛选标签右边的"清除已完成"按钮消失(因为没有已完成任务了)。

8.6 空状态

进入"已完成"筛选模式,点击"清除已完成"。列表显示空状态:“暂无已完成任务” + “完成任务后勾选即可”。

九、Checkbox vs Radio 的选择

本文是 Demo 24(Radio 健康评估问卷)和 Demo 25(Checkbox 任务清单)的组合,两篇文章构成了"单选 vs 多选"的对照学习路径。以下是两者的核心差异总结:

维度 Radio Checkbox
选择模式 单选(互斥) 多选(独立)
选中 API .checked(boolean) .select(boolean)
group 语义 组内互斥——同组只能选一个 组内独立——同组可多选
典型场景 单选题、性别、支付方式 待办勾选、批量选择、权限设置
状态管理 一个 @State 变量(当前选中项索引) 多个 @State 变量(每项的布尔状态)或布尔数组
全选/全不选 无此概念 可通过遍历组内 Checkbox 实现

理解两者的 API 差异和使用场景,是构建移动端表单的基本功。

十、总结

本文通过"任务清单"这个实战案例,全面讲解了 ArkUI Checkbox 复选框组件的使用方法。核心知识点包括:

  1. Checkbox 基础 APIname + group 分组 + .select() 状态控制 + .onChange() 状态变化回调
  2. 状态管理:使用 @State 数组 + TaskItem.toggleDone() 不可变更新模式
  3. 筛选机制:三态筛选(全部/未完成/已完成)+ getFilteredTasks() 计算属性
  4. 添加/清除操作trim() 输入验证 + slice().concat() 不可变添加 + slice().filter() 批量删除
  5. 视觉反馈:完成任务的四重变化(颜色变浅、字重变轻、删除线、背景微蓝)
  6. 空状态设计:三种空状态的差异化提示文字
  7. 进度统计:标题栏实时完成计数(done/total)
  8. Checkbox vs Radio:单选与多选的 API 差异和场景选择

复选框是比单选更灵活的交互组件。一个好的复选框体验不仅需要明确的选中/未选中视觉状态,还需要合理的数据模型(可扩展的不可变更新)、流畅的筛选切换和清晰的批量操作入口。ArkUI 的 Checkbox 组件提供了简洁的 API,让开发者将精力集中在任务逻辑和用户体验的设计上。


Logo

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

更多推荐