摘要:列表滑动删除是移动应用中极为常见的交互模式。本文详细讲解如何在 HarmonyOS ArkTS 中实现带有切换删除按钮的列表,支持添加和删除列表项,涵盖 List 组件、手势交互、状态管理和动画反馈等核心技术点。
一、项目概述
「滑动删除」功能是移动端列表交互的标配------用户在列表项上向左滑动,露出删除按钮,点击即可删除该项。同时,我们也提供「添加」功能,让用户可以动态扩充列表。
本项目的核心功能点:
| 功能 | 描述 | 技术实现 |
|---|---|---|
| 列表展示 | 以列表形式展示数据项 | List + ListItem |
| 滑动显示删除 | 左滑列表项露出删除按钮 | 手势监听 + 状态控制 |
| 删除确认 | 点击删除按钮移除列表项 | splice 删除 + 状态更新 |
| 新增项 | 添加新的列表条目 | 数组 push + List 刷新 |
| 删除按钮切换 | 点击按钮切换为删除模式 | Toggle 状态驱动 |
二、项目架构
entry/src/main/ets/
├── pages/
│ └── SwipeDelete.ets // 滑动删除主页面
├── components/
│ ├── SwipeableItem.ets // 可滑动列表项组件
│ └── AddItemDialog.ets // 添加项目的弹窗组件
├── model/
│ └── ListItemData.ets // 列表项数据模型
└── common/
└── Constants.ets // 常量与样式定义
数据流架构:
用户手势 → SwipeableItem (位移) → 父组件 (删除/添加) → @State 更新 → UI 重渲染
三、核心技术分析
3.1 List 组件与列表渲染
HarmonyOS 的 List 组件是高性能列表容器,配合 ListItem 和 ForEach 实现数据驱动渲染:
arkts
// model/ListItemData.ets
export interface ListItemData {
id: number
title: string
description: string
time: string
isFavorite: boolean
}
export function createMockData(): ListItemData[] {
const items: ListItemData[] = []
const titles = [
'HarmonyOS 开发笔记', 'ArkTS 布局技巧', '状态管理指南',
'组件化实践', '网络请求封装', '数据持久化方案',
'动画与过渡', '性能优化专题', '调试工具使用',
'发布上架流程'
]
for (let i = 0; i < titles.length; i++) {
items.push({
id: i + 1,
title: titles[i],
description: `第 ${i + 1} 篇技术文章`,
time: `2025-01-${String(i + 10).padStart(2, '0')}`,
isFavorite: i % 3 === 0
})
}
return items
}
ForEach 渲染列表:
arkts
List({ space: 10 }) {
ForEach(this.items, (item: ListItemData, index: number) => {
ListItem() {
SwipeableItem({
item: item,
onDelete: () => this.deleteItem(index),
onToggle: () => this.toggleFavorite(index)
})
}
})
}
.width('100%')
.height('100%')
.divider({ strokeWidth: 1, color: '#E8E8E8' })
List 组件关键属性:
| 属性 | 作用 | 本场景用法 |
|---|---|---|
space |
列表项间距 | 10vp,增加呼吸感 |
divider |
分割线 | 浅灰色 1px 分割线 |
edgeEffect |
边缘弹性效果 | EdgeEffect.Spring |
scrollBar |
滚动条 | 自动显示 |
3.2 滑动删除手势实现
滑动删除的核心是手势处理。在 ArkTS 中,我们可以通过 onTouch 事件或 PanGesture 手势实现滑动检测。这里我们采用 状态驱动 的方式:
arkts
// components/SwipeableItem.ets
@Component
export struct SwipeableItem {
@Prop item: ListItemData
private onDelete?: () => void
private onToggle?: () => void
@State private offsetX: number = 0
@State private isSwiped: boolean = false
private readonly DELETE_BTN_WIDTH: number = 80
private readonly SWIPE_THRESHOLD: number = 60
private startX: number = 0
build() {
Stack() {
// 底层:删除按钮
Row() {
Button('删除')
.width(this.DELETE_BTN_WIDTH)
.height('100%')
.backgroundColor('#FF3B30')
.fontColor(Color.White)
.fontSize(15)
.onClick(() => {
if (this.onDelete) {
this.onDelete()
}
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
// 顶层:列表项内容(可滑动)
Row() {
// 图标
Circle({ width: 40, height: 40 })
.fill(this.item.isFavorite ? '#FFD700' : '#E0E0E0')
.margin({ left: 16, right: 12 })
// 文本内容
Column({ space: 4 }) {
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.lineHeight(22)
Text(this.item.description)
.fontSize(13)
.fontColor('#888888')
Text(this.item.time)
.fontSize(12)
.fontColor('#AAAAAA')
}
.alignItems(HorizontalAlign.Start)
// 收藏按钮
Button({ type: ButtonType.Circle }) {
Image(this.item.isFavorite ? $r('app.media.star_filled') : $r('app.media.star_empty'))
.width(24)
.height(24)
}
.width(36)
.height(36)
.backgroundColor('transparent')
.margin({ right: 12 })
.onClick(() => {
if (this.onToggle) {
this.onToggle()
}
})
}
.width('100%')
.height(72)
.backgroundColor(Color.White)
.borderRadius(8)
.translate({ x: this.offsetX })
.onTouch((event: TouchEvent) => {
this.handleTouch(event)
})
}
.width('100%')
.height(72)
.clip(true) // 超出部分裁切,保持圆角
}
handleTouch(event: TouchEvent): void {
switch (event.type) {
case TouchType.Down:
this.startX = event.touches[0].x
break
case TouchType.Move:
const currentX = event.touches[0].x
const deltaX = currentX - this.startX
// 只允许向左滑动(负方向)
const newOffset = Math.min(0, Math.max(-this.DELETE_BTN_WIDTH, this.offsetX + deltaX))
this.offsetX = newOffset
this.startX = currentX
break
case TouchType.Up:
case TouchType.Cancel:
if (this.offsetX < -this.SWIPE_THRESHOLD) {
// 滑过阈值,吸附到删除按钮完全显示
this.offsetX = -this.DELETE_BTN_WIDTH
this.isSwiped = true
} else {
// 未达阈值,回弹到原始位置
this.offsetX = 0
this.isSwiped = false
}
break
}
}
}
滑动逻辑详解:
- TouchType.Down:记录触摸起始位置
- TouchType.Move :计算手指位移,更新
offsetX,限制范围在[-DELETE_BTN_WIDTH, 0] - TouchType.Up :判断是否超过阈值
SWIPE_THRESHOLD(60vp)- 超过:吸附到删除按钮完全显示位置(
-DELETE_BTN_WIDTH) - 未超过:弹性回弹到原始位置(
0)
- 超过:吸附到删除按钮完全显示位置(
- clip(true):裁切超出 Stack 区域的内容,保持圆角效果
3.3 删除与添加功能
删除操作
arkts
// SwipeDelete.ets 中的删除方法
deleteItem(index: number): void {
// 动画效果可以先移除
const deletedItem = this.items[index]
this.items.splice(index, 1)
// 更新状态触发 UI 刷新
this.items = [...this.items]
// 显示提示
this.showToast(`已删除「${deletedItem.title}」`)
}
注意事项:
- 使用
splice删除后,需要重新赋值以触发@State更新 - 使用扩展运算符
[...this.items]创建新数组引用,确保 ArkTS 检测到变化 - 删除后显示 Toast 提示用户操作结果
添加操作
arkts
// 添加新项目
addItem(title: string, description: string): void {
const newItem: ListItemData = {
id: Date.now(), // 使用时间戳生成唯一 ID
title: title,
description: description,
time: new Date().toLocaleDateString('zh-CN'),
isFavorite: false
}
this.items = [newItem, ...this.items] // 插到列表顶部
this.showToast('已添加新项目')
}
添加弹窗组件:
arkts
// components/AddItemDialog.ets
@Component
export struct AddItemDialog {
@State private title: string = ''
@State private description: string = ''
private onConfirm?: (title: string, desc: string) => void
private onCancel?: () => void
@State private visible: boolean = false
build() {
if (this.visible) {
Stack() {
// 遮罩层
Rect()
.width('100%')
.height('100%')
.fill('#80000000')
.onClick(() => this.dismiss())
// 对话框
Column({ space: 16 }) {
Text('添加新项目')
.fontSize(20)
.fontWeight(FontWeight.Bold)
TextInput({ placeholder: '标题', text: this.title })
.width('100%')
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding({ left: 12 })
.onChange((v: string) => { this.title = v })
TextInput({ placeholder: '描述', text: this.description })
.width('100%')
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding({ left: 12 })
.onChange((v: string) => { this.description = v })
Row({ space: 16 }) {
Button('取消')
.layoutWeight(1)
.height(40)
.backgroundColor('#E0E0E0')
.fontColor('#333333')
.borderRadius(8)
.onClick(() => this.dismiss())
Button('确认')
.layoutWeight(1)
.height(40)
.backgroundColor('#007AFF')
.fontColor(Color.White)
.borderRadius(8)
.onClick(() => {
if (this.title.trim().length > 0 && this.onConfirm) {
this.onConfirm(this.title.trim(), this.description.trim())
this.dismiss()
}
})
}
.width('100%')
}
.width('85%')
.padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
}
.width('100%')
.height('100%')
}
}
show(): void {
this.title = ''
this.description = ''
this.visible = true
}
dismiss(): void {
this.visible = false
}
}
3.4 Toggle 删除按钮切换
除了滑动删除,我们还实现一个「切换删除模式」的按钮,让用户通过点击进入删除状态:
arkts
// 主页面中的 Toggle 按钮
@State private isDeleteMode: boolean = false
build() {
Column() {
// 顶部栏
Row() {
Text('滑动删除列表')
.fontSize(22)
.fontWeight(FontWeight.Bold)
Blank()
// 切换删除模式
Toggle({ type: ToggleType.Switch, isOn: this.isDeleteMode })
.onChange((isOn: boolean) => {
this.isDeleteMode = isOn
})
Text(this.isDeleteMode ? '删除模式' : '滑动模式')
.fontSize(13)
.fontColor(this.isDeleteMode ? '#FF3B30' : '#888888')
// 添加按钮
Button({ type: ButtonType.Circle }) {
Image($r('app.media.add_icon'))
.width(24)
.height(24)
}
.width(36)
.height(36)
.backgroundColor('#007AFF')
.margin({ left: 8 })
.onClick(() => {
this.dialog.show()
})
}
.width('92%')
.padding({ top: 16, bottom: 8 })
// 列表
List({ space: 10 }) {
ForEach(this.items, (item: ListItemData, index: number) => {
ListItem() {
if (this.isDeleteMode) {
// 删除模式下:显示删除按钮
Row() {
Text(item.title)
.fontSize(16)
.layoutWeight(1)
Button('删除')
.width(60)
.height(32)
.backgroundColor('#FF3B30')
.fontColor(Color.White)
.fontSize(13)
.borderRadius(6)
.onClick(() => this.deleteItem(index))
}
.width('100%')
.height(56)
.padding({ left: 16, right: 8 })
.backgroundColor(Color.White)
.borderRadius(8)
} else {
// 滑动模式下:使用可滑动组件
SwipeableItem({
item: item,
onDelete: () => this.deleteItem(index),
onToggle: () => this.toggleFavorite(index)
})
}
}
})
}
.width('92%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F0F0F0')
}
Toggle 组件说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| type | ToggleType | Switch / Checkbox / Button |
| isOn | boolean | 当前开关状态 |
| onChange | callback | 状态变化回调 |
通过 Toggle 切换两种模式,用户可以选择自己习惯的操作方式。
四、HarmonyOS 特性深度解析
4.1 手势系统
ArkTS 的手势系统支持多种手势识别器:
| 手势 | 说明 | 适用场景 |
|---|---|---|
TapGesture |
点击 | 按钮点击 |
LongPressGesture |
长按 | 触发编辑模式 |
PanGesture |
拖拽 | 滑动删除 |
SwipeGesture |
快速滑动 | 翻页 |
PinchGesture |
双指缩放 | 图片缩放 |
本例使用 onTouch 事件实现滑动,因为其提供了更细粒度的触摸控制。也可以改用 PanGesture 手势:
arkts
SwipeableItem()
.gesture(
PanGesture({ direction: PanDirection.Left })
.onActionStart((event: GestureEvent) => { ... })
.onActionUpdate((event: GestureEvent) => { ... })
.onActionEnd((event: GestureEvent) => { ... })
)
4.2 translate 属性与动画
translate 属性用于移动组件的渲染位置,不影响布局占位:
arkts
.translate({ x: this.offsetX })
结合 animateTo 可以实现平滑动画:
arkts
// 回弹动画
animateTo({ duration: 200, curve: Curve.Friction }, () => {
this.offsetX = 0
})
曲线类型对比:
| Curve | 效果 | 使用场景 |
|---|---|---|
| Linear | 匀速 | 简单移动 |
| Friction | 带阻尼的减速 | 自然回弹 |
| Ease | 渐入渐出 | 平滑动画 |
| Spring | 弹簧效果 | 弹性交互 |
4.3 clip 属性裁切
clip 属性控制内容超出容器范围时的显示方式:
arkts
Stack()
.clip(true) // 裁切超出部分
// 或
.clip(false) // 允许内容溢出
在滑动删除中,clip(true) 保证删除按钮在列表项圆角范围内优雅显示。
五、UI/UX 设计要点
5.1 交互反馈设计
- 视觉反馈:滑动时列表项跟随手指移动,提供实时位置反馈
- 触感反馈 :删除时调用震动 API
vibrator.vibrate(30) - Toast 提示:操作完成后显示轻量提示
- 回弹动画:未达到阈值时弹性回弹,模拟真实物理效果
5.2 删除确认机制
为防止误操作,可添加二次确认:
arkts
deleteWithConfirm(index: number): void {
AlertDialog.show({
title: '确认删除',
message: `确定要删除「${this.items[index].title}」吗?`,
primaryButton: {
value: '取消',
action: () => {}
},
secondaryButton: {
value: '删除',
action: () => {
this.deleteItem(index)
}
}
})
}
5.3 空状态展示
当列表为空时,显示友好的空状态提示:
arkts
if (this.items.length === 0) {
Column() {
Image($r('app.media.empty_icon'))
.width(120)
.height(120)
Text('暂无数据,点击右上角添加')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
六、最佳实践总结
6.1 性能优化
-
key 属性 :为
ForEach提供唯一 key,提升 diff 效率arktsForEach(this.items, (item: ListItemData) => { ListItem() { ... } }, (item: ListItemData) => item.id.toString()) -
避免全量重建:数组操作后使用浅拷贝触发更新,而非重新创建整个数组
-
LazyForEach :大量数据时使用
LazyForEach进行虚拟列表渲染
6.2 错误处理
- 边界情况:空列表删除、删除最后一个项目、快速连续删除
- 同步状态:确保删除后列表索引与数据状态一致
- 异常恢复:删除失败时恢复数据
6.3 无障碍访问
- 为删除按钮添加
accessibilityText描述 - 确保滑动操作有替代的按钮操作方式
- 使用
Toggle提供模式切换,兼顾不同用户习惯
七、扩展与演进
7.1 多选批量删除
arkts
@State selectedIds: Set<number> = new Set()
toggleSelection(id: number): void {
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id)
} else {
this.selectedIds.add(id)
}
}
batchDelete(): void {
this.items = this.items.filter(item => !this.selectedIds.has(item.id))
this.selectedIds.clear()
}
7.2 拖拽排序
配合 onDragStart / onDragEnter 事件实现拖拽排序:
arkts
ListItem()
.onDragStart(() => { ... })
.onDrop((event: DragEvent) => { ... })
7.3 撤销删除
使用栈结构保存删除历史:
arkts
private undoStack: ListItemData[] = []
deleteItem(index: number): void {
const deleted = this.items[index]
this.undoStack.push(deleted)
this.items.splice(index, 1)
this.items = [...this.items]
showToast('已删除(可撤销)')
}
undoDelete(): void {
if (this.undoStack.length > 0) {
const restored = this.undoStack.pop()
this.items = [restored!, ...this.items]
}
}
八、结语
滑动删除列表是移动应用中最常见也最实用的交互模式之一。通过本项目的学习,我们掌握了:
- List + ForEach 构建数据驱动列表
- onTouch 事件实现手势滑动检测
- translate 属性实现内容位移
- Toggle 组件切换操作模式
- 状态管理在增删操作中的应用
这些技能在笔记应用、任务管理、消息列表、购物车等场景中广泛适用,是 ArkTS 开发者的必会技能。