【HarmonyOS 6】打卡列表实战:复选框与列表的完美结合
/ 打卡项列表// 已完成数量使用 @State 装饰器确保状态变化时 UI 自动更新。Checkbox 组件的基本用法和属性配置列表项状态管理的核心逻辑(创建新数组触发更新)完成/未完成状态的视觉区分(透明度、背景色)自动排序算法(未完成在前,已完成在后)进度统计与显示整行可点击的交互优化掌握这些技巧后,你可以轻松实现各种带复选框的列表场景,如待办事项、任务清单、多选列表等。
一、案例背景
在健康管理 APP 的每日打卡模块中,我们需要实现一个交互友好的打卡列表:
- 使用 Checkbox 组件实现打卡状态切换
- 点击列表项或复选框都能切换状态
- 已完成项自动排序到列表底部
- 完成项显示半透明效果,视觉上弱化
- 实时统计完成进度并显示百分比

本文将聚焦 Checkbox 组件的使用,详细讲解如何实现这个功能完善的打卡列表。
二、完整代码实现
@Component
export struct CheckInTabContent {
@State checkInItems: CheckInItemData[] = [];
@State completedCount: number = 0;
aboutToAppear(): void {
this.loadCheckInData();
}
loadCheckInData(): void {
// 从数据服务加载打卡数据
this.checkInItems = [
{ id: 'early_sleep', name: '早睡', icon: '🌙', isChecked: false },
{ id: 'early_wake', name: '早起', icon: '🌅', isChecked: true },
{ id: 'drink_water', name: '喝水', icon: '💧', isChecked: false },
{ id: 'exercise', name: '运动', icon: '🏃', isChecked: true },
{ id: 'breakfast', name: '吃早餐', icon: '🍳', isChecked: false },
{ id: 'study', name: '学习', icon: '📚', isChecked: false }
];
this.completedCount = this.checkInItems.filter(
(item: CheckInItemData): boolean => item.isChecked
).length;
}
// 获取排序后的打卡项(未完成在前,已完成在后)
getSortedCheckInItems(): CheckInItemData[] {
const unchecked: CheckInItemData[] = [];
const checked: CheckInItemData[] = [];
for (let i = 0; i < this.checkInItems.length; i++) {
if (this.checkInItems[i].isChecked) {
checked.push(this.checkInItems[i]);
} else {
unchecked.push(this.checkInItems[i]);
}
}
return unchecked.concat(checked);
}
// 通过ID切换打卡状态
toggleCheckInById(itemId: string): void {
let targetIndex = -1;
for (let i = 0; i < this.checkInItems.length; i++) {
if (this.checkInItems[i].id === itemId) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) return;
// 创建新数组以触发UI更新
const newItems: CheckInItemData[] = [];
for (let i = 0; i < this.checkInItems.length; i++) {
const item = this.checkInItems[i];
newItems.push({
id: item.id,
name: item.name,
icon: item.icon,
isChecked: i === targetIndex ? !item.isChecked : item.isChecked
});
}
this.checkInItems = newItems;
this.completedCount = this.checkInItems.filter(
(item: CheckInItemData): boolean => item.isChecked
).length;
}
build() {
Column() {
// 标题
Row() {
Text('每日打卡')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
}
.width('100%')
.padding(16)
// 进度显示
Row() {
Text(`已完成 ${this.completedCount}/${this.checkInItems.length} 项`)
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
Blank()
Text(`${Math.round((this.completedCount / Math.max(this.checkInItems.length, 1)) * 100)}%`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.primary_color'))
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
Progress({
value: this.completedCount,
total: Math.max(this.checkInItems.length, 1),
type: ProgressType.Linear
})
.color($r('app.color.primary_color'))
.height(8)
.borderRadius(4)
.margin({ left: 16, right: 16, bottom: 16 })
// 打卡列表
List() {
ForEach(this.getSortedCheckInItems(), (item: CheckInItemData) => {
ListItem() {
Row() {
Text(item.icon)
.fontSize(24)
.opacity(item.isChecked ? 0.5 : 1)
Text(item.name)
.fontSize(16)
.fontColor($r('app.color.text_primary'))
.margin({ left: 12 })
.layoutWeight(1)
Checkbox()
.select(item.isChecked)
.selectedColor($r('app.color.primary_color'))
.onChange((value: boolean) => {
this.toggleCheckInById(item.id);
})
}
.width('100%')
.padding(16)
.backgroundColor(
item.isChecked
? $r('app.color.secondary_background')
: $r('app.color.card_background')
)
.borderRadius(12)
.onClick(() => {
this.toggleCheckInById(item.id);
})
}
.margin({ left: 16, right: 16, bottom: 8 })
}, (item: CheckInItemData): string => `${item.id}_${item.isChecked}`)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background_color'))
}
}
interface CheckInItemData {
id: string;
name: string;
icon: string;
isChecked: boolean;
}
三、Checkbox 组件详解
3.1 Checkbox 基本属性
Checkbox()
.select(false) // 是否选中
.selectedColor($r('app.color.primary_color')) // 选中时的颜色
.onChange((value: boolean) => { // 状态变化回调
console.log('新状态:', value);
})
核心属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| select | boolean | 控制复选框是否选中 |
| selectedColor | ResourceColor | 选中状态的颜色 |
| onChange | (value: boolean) => void | 状态变化时的回调函数 |
3.2 Checkbox 在列表中的应用
Checkbox()
.select(item.isChecked) // 绑定数据状态
.selectedColor($r('app.color.primary_color'))
.onChange((value: boolean) => {
this.toggleCheckInById(item.id); // 触发状态切换
})

3.3 Checkbox 样式定制
Checkbox()
.select(item.isChecked)
.selectedColor($r('app.color.primary_color'))
.width(24) // 设置宽度
.height(24) // 设置高度
虽然 Checkbox 样式定制选项有限,但可以通过颜色和尺寸调整来适配设计风格。
四、列表项状态管理
4.1 状态定义
@State checkInItems: CheckInItemData[] = []; // 打卡项列表
@State completedCount: number = 0; // 已完成数量
使用 @State 装饰器确保状态变化时 UI 自动更新。
4.2 状态切换的核心逻辑
关键点:必须创建新数组才能触发 UI 更新
toggleCheckInById(itemId: string): void {
// 1. 找到目标项的索引
let targetIndex = -1;
for (let i = 0; i < this.checkInItems.length; i++) {
if (this.checkInItems[i].id === itemId) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) return;
// 2. 创建新数组(这是触发UI更新的关键)
const newItems: CheckInItemData[] = [];
for (let i = 0; i < this.checkInItems.length; i++) {
const item = this.checkInItems[i];
newItems.push({
id: item.id,
name: item.name,
icon: item.icon,
isChecked: i === targetIndex ? !item.isChecked : item.isChecked
});
}
// 3. 更新状态
this.checkInItems = newItems;
// 4. 重新计算完成数量
this.completedCount = this.checkInItems.filter(
(item: CheckInItemData): boolean => item.isChecked
).length;
}
状态更新流程图:
用户点击 Checkbox 或列表项
↓
触发 onChange 回调
↓
调用 toggleCheckInById()
↓
查找目标项索引
↓
创建新数组并修改状态
↓
更新 @State 变量
↓
UI 自动重新渲染
↓
显示新的选中状态
4.3 为什么必须创建新数组?
错误示例:
// ❌ 直接修改数组元素,UI 不会更新
toggleCheckInById(itemId: string): void {
const index = this.checkInItems.findIndex(item => item.id === itemId);
this.checkInItems[index].isChecked = !this.checkInItems[index].isChecked;
// UI 不会更新!
}
正确示例:
// ✅ 创建新数组,UI 会更新
toggleCheckInById(itemId: string): void {
const newItems = this.checkInItems.map(item => {
if (item.id === itemId) {
return { ...item, isChecked: !item.isChecked };
}
return item;
});
this.checkInItems = newItems; // 触发 UI 更新
}
ArkTS 的响应式系统通过对象引用来检测变化,只有当 @State 变量的引用改变时,才会触发 UI 更新。
五、完成/未完成样式区分
5.1 视觉差异设计
通过不同的视觉效果区分已完成和未完成的打卡项:
Row() {
// 图标透明度
Text(item.icon)
.fontSize(24)
.opacity(item.isChecked ? 0.5 : 1) // 已完成:半透明
Text(item.name)
.fontSize(16)
.fontColor($r('app.color.text_primary'))
.margin({ left: 12 })
.layoutWeight(1)
Checkbox()
.select(item.isChecked)
.selectedColor($r('app.color.primary_color'))
}
.backgroundColor(
item.isChecked
? $r('app.color.secondary_background') // 已完成:浅色背景
: $r('app.color.card_background') // 未完成:卡片背景
)
.borderRadius(12)
5.2 样式对比表
| 元素 | 未完成状态 | 已完成状态 | 视觉效果 |
|---|---|---|---|
| 图标透明度 | 1.0 | 0.5 | 已完成项图标变暗 |
| 背景颜色 | card_background | secondary_background | 已完成项背景变浅 |
| 复选框 | 未选中 | 选中(主题色) | 明确的状态标识 |
5.3 样式实现技巧
使用三元运算符根据状态动态设置样式:
.opacity(item.isChecked ? 0.5 : 1)
.backgroundColor(item.isChecked ? $r('app.color.secondary_background') : $r('app.color.card_background'))
这种方式简洁明了,易于维护。
六、列表自动排序
6.1 排序需求
将已完成的打卡项自动移到列表底部,让用户优先看到未完成的任务。
排序前: 排序后:
✓ 早睡 ○ 喝水
○ 喝水 ○ 吃早餐
✓ 运动 ○ 学习
○ 吃早餐 ✓ 早睡
○ 学习 ✓ 运动
6.2 排序算法实现
getSortedCheckInItems(): CheckInItemData[] {
const unchecked: CheckInItemData[] = [];
const checked: CheckInItemData[] = [];
// 遍历分类
for (let i = 0; i < this.checkInItems.length; i++) {
if (this.checkInItems[i].isChecked) {
checked.push(this.checkInItems[i]);
} else {
unchecked.push(this.checkInItems[i]);
}
}
// 未完成在前,已完成在后
return unchecked.concat(checked);
}
算法步骤:
- 创建两个空数组:unchecked(未完成)和 checked(已完成)
- 遍历原始列表,根据 isChecked 状态分类
- 将未完成数组和已完成数组拼接,返回新数组
6.3 在 ForEach 中使用排序
List() {
ForEach(
this.getSortedCheckInItems(), // 使用排序后的数据
(item: CheckInItemData) => {
ListItem() {
// 列表项内容
}
},
(item: CheckInItemData): string => `${item.id}_${item.isChecked}`
)
}
注意: 键值生成器使用 ${item.id}_${item.isChecked},确保状态变化时列表项能正确更新。
6.4 排序的用户体验
- 未完成项始终在顶部,用户一眼就能看到待办任务
- 已完成项沉底,视觉上不会干扰用户
- 配合半透明样式,已完成项进一步弱化
- 排序是自动的,用户无需手动操作
七、进度统计与显示
7.1 完成数量统计
使用 filter 方法统计已完成项:
this.completedCount = this.checkInItems.filter(
(item: CheckInItemData): boolean => item.isChecked
).length;
7.2 完成百分比计算
const percentage = Math.round(
(this.completedCount / Math.max(this.checkInItems.length, 1)) * 100
);
注意: 使用 Math.max(this.checkInItems.length, 1) 避免除以零错误。
7.3 进度显示 UI
// 文字进度
Row() {
Text(`已完成 ${this.completedCount}/${this.checkInItems.length} 项`)
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
Blank() // 占据中间空间
Text(`${Math.round((this.completedCount / Math.max(this.checkInItems.length, 1)) * 100)}%`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.primary_color'))
}
// 进度条
Progress({
value: this.completedCount,
total: Math.max(this.checkInItems.length, 1),
type: ProgressType.Linear
})
.color($r('app.color.primary_color'))
.height(8)
.borderRadius(4)

八、整行可点击优化
8.1 提升交互体验
不仅复选框可以点击,整个列表项都可以点击来切换状态:
Row() {
Text(item.icon)
Text(item.name)
Checkbox()
.onChange((value: boolean) => {
this.toggleCheckInById(item.id);
})
}
.onClick(() => {
this.toggleCheckInById(item.id); // 整行可点击
})
8.2 避免重复触发
由于整行和 Checkbox 都绑定了点击事件,需要确保它们调用同一个方法,避免状态混乱:
// ✅ 正确:都调用同一个方法
.onClick(() => {
this.toggleCheckInById(item.id);
})
Checkbox()
.onChange((value: boolean) => {
this.toggleCheckInById(item.id);
})
九、总结
本文通过健康管理 APP 的打卡列表案例,深入讲解了 Checkbox 组件在列表中的应用:
- Checkbox 组件的基本用法和属性配置
- 列表项状态管理的核心逻辑(创建新数组触发更新)
- 完成/未完成状态的视觉区分(透明度、背景色)
- 自动排序算法(未完成在前,已完成在后)
- 进度统计与显示
- 整行可点击的交互优化
掌握这些技巧后,你可以轻松实现各种带复选框的列表场景,如待办事项、任务清单、多选列表等。
更多推荐

所有评论(0)