鸿蒙新特性:Checkbox 复选框组件——构建任务清单
复选框是多选交互的基础。待办事项的勾选完成、批量选择后的操作、权限设置中的多项启用——所有这些场景都需要复选框来承载。与 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 参数表示变化后的选中状态(true 或 false)。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 包含 total、done、active 三个字段,分别表示任务总数、已完成数和未完成数。
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();
}
每当 filter 或 tasks 变化时,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 });
}
流程分五步:
trim()去除首尾空格,如果为空直接返回- 用
new Date()获取当前时间,通过.getHours()和.getMinutes()格式化为HH:MM的时间字符串 - 创建新
TaskItem,状态为done = false(未完成) - 通过
slice().concat(task)创建新数组并赋值给@State tasks,触发 UI 刷新 - 清空输入框,显示 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')
已完成的任务有四个视觉变化:
- 文字颜色变浅(
#BBBBCCvs#1a1a2e) - 字重变轻(
FontWeight.NormalvsFontWeight.Medium) - 删除线(
TextDecorationType.LineThrough) - 行背景微蓝(
#FAFCFFvs#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())提交——这避免了空按钮占据空间的尴尬。
双提交路径:
- 点击"添加"按钮 → 触发
addTask() - 键盘按回车键 →
.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 复选框组件的使用方法。核心知识点包括:
- Checkbox 基础 API:
name+group分组 +.select()状态控制 +.onChange()状态变化回调 - 状态管理:使用
@State数组 +TaskItem.toggleDone()不可变更新模式 - 筛选机制:三态筛选(全部/未完成/已完成)+
getFilteredTasks()计算属性 - 添加/清除操作:
trim()输入验证 +slice().concat()不可变添加 +slice().filter()批量删除 - 视觉反馈:完成任务的四重变化(颜色变浅、字重变轻、删除线、背景微蓝)
- 空状态设计:三种空状态的差异化提示文字
- 进度统计:标题栏实时完成计数(done/total)
- Checkbox vs Radio:单选与多选的 API 差异和场景选择
复选框是比单选更灵活的交互组件。一个好的复选框体验不仅需要明确的选中/未选中视觉状态,还需要合理的数据模型(可扩展的不可变更新)、流畅的筛选切换和清晰的批量操作入口。ArkUI 的 Checkbox 组件提供了简洁的 API,让开发者将精力集中在任务逻辑和用户体验的设计上。
更多推荐




所有评论(0)