HarmonyOS ArkUI List 的侧滑删除 用 ListItem 的 swipeAction + endIcon 的做法
鸿蒙的 List 组件自带侧滑交互,但 API 不直观——是通过 ListItem.swipeAction 配 start 和 end 两个 builder 实现的。做了几个 app 的列表都要加"侧滑删除"和"侧滑归档",走通后发现 pattern 很固定。记录一下 swipeAction + ListItem 的做法,顺便分享给有需要的朋友。
一、基础 pattern
@Entry @Component
struct AssetListPage {
@State items: Asset[] = []
build() {
List({ space: 10 }) {
ForEach(this.items, (item: Asset) => {
ListItem() {
AssetRow({ asset: item })
}
.swipeAction({
end: {
builder: () => this.endSwipeBuilder(item),
actionAreaDistance: 100,
onAction: () => this.deleteItem(item.id),
},
})
}, (item: Asset) => item.id.toString())
}
}
@Builder
endSwipeBuilder(item: Asset) {
Row() {
Column() {
Image($r('app.media.ic_delete')).width(24).height(24)
Text('删除').fontSize(12).fontColor(Color.White)
}
.width(80)
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#EF4444')
}
}
private async deleteItem(id: number): Promise<void> {
await this.dao.delete(id)
this.items = await this.dao.getAll()
}
}
关键字段:
end: { builder, actionAreaDistance, onAction }:侧滑到左边(end = 滑到结束方向)。
- builder:滑开后显示的 UI
- actionAreaDistance:滑开阈值(100 vp)
- onAction:滑开后用户再次点击时触发
start:滑到右边(可选)。本案没用。
二、侧滑的交互语义
用户动作 → 效果:
| 动作 | 结果 |
|---|---|
| 轻滑(<100vp) | 回弹,不显示 action |
| 滑开 >100vp | 停住,显示 action |
| 滑开后点 action | 触发 onAction |
| 滑开后点列表本身 | 回弹 |
| 滑过阈值 2 倍(>200vp) | 直接触发 onAction(省一次点击) |
用户体验:
- 怕误触 → 要滑开 + 再点(两步)
- 效率党 → 直接滑到底(一步)
本案的 100 vp 阈值:手感合适。
- 50 vp 太敏感
- 150 vp 太费力
三、双侧 action(左删 右归档)
.swipeAction({
start: {
builder: () => this.startSwipeBuilder(item),
actionAreaDistance: 100,
onAction: () => this.archiveItem(item.id),
},
end: {
builder: () => this.endSwipeBuilder(item),
actionAreaDistance: 100,
onAction: () => this.deleteItem(item.id),
},
})
@Builder
startSwipeBuilder(item: Asset) {
Column() {
Image($r('app.media.ic_archive')).width(24).height(24)
Text('归档').fontSize(12).fontColor(Color.White)
}
.width(80)
.height('100%')
.backgroundColor('#3B82F6') // 蓝色
}
左右两套 action:
- start(右滑):归档(蓝)
- end(左滑):删除(红)
色彩约定:
- 红 → 危险 / 删除
- 蓝 → 操作 / 归档
- 绿 → 确认 / 完成
独立开发者的建议:红色永远是删除。不要用红色做其他 action(用户习惯了)。
四、滑动删除后的 UI 更新
private async deleteItem(id: number): Promise<void> {
await this.dao.delete(id)
this.items = await this.dao.getAll() // 👈 整数组重新赋值
}
关键:整数组重新赋值,不是原地 splice。
不要:
// ❌ 原地改
const idx = this.items.findIndex(i => i.id === id)
this.items.splice(idx, 1)
ArkUI 的 List 对数组引用响应,splice 可能不触发 rebuild。
正解:整数组赋值(本案)或用 immutable 的 filter:
this.items = this.items.filter(i => i.id !== id)
ForEach 的 key(第三参数):
ForEach(this.items, (item: Asset) => {
// ...
}, (item: Asset) => item.id.toString())
key 的作用:ArkUI 识别哪些 item 变了,只 rebuild 变化的。
- 没 key → 全部 rebuild(卡)
- 有 key → 只 rebuild 变化的(流畅)
独立开发者的建议:ForEach 的 key 永远指定,用 id 字段。
五、动画:删除时淡出
朴素做法:删了直接消失,生硬。
优化:animateTo 淡出 + 移除。
private async deleteItem(id: number): Promise<void> {
// 1. 先标记删除中
const idx = this.items.findIndex(i => i.id === id)
this.items[idx].pendingDelete = true
// 2. 等动画完成
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
this.items = this.items.filter(i => !i.pendingDelete)
})
// 3. 异步同步 DB
await this.dao.delete(id)
}
UI:
ListItem() {
AssetRow({ asset: item })
}
.opacity(item.pendingDelete ? 0 : 1)
.height(item.pendingDelete ? 0 : undefined) // 👈 高度塌陷
.animation({ duration: 300 })
用户看到:item 淡出 + 列表平滑合拢。
独立开发者的建议:删除动画是app 精致度的关键。1-2 行代码的差别,用户感知的 "app 质感" 翻倍。
六、确认对话框:防止误删
重要数据(如联系人、笔记)不要滑开就删:
.swipeAction({
end: {
builder: () => this.endSwipeBuilder(item),
onAction: () => {
promptAction.showDialog({
title: '删除确认',
message: `确定删除「${item.title}」吗?此操作不可撤销。`,
buttons: [
{ text: '取消', color: '#999' },
{ text: '删除', color: '#EF4444' },
],
}).then((result) => {
if (result.index === 1) {
this.deleteItem(item.id)
}
})
},
},
})
用户 flow:滑开 → 点删除 → 弹对话框 → 再确认。三步。
不要:不重要的数据也弹对话框。打断用户。
决策:
- 轻量数据(to-do / 书摘):滑开直接删(可撤销)
- 重要数据(联系人 / 账单):滑开 + 确认
七、Undo(撤销删除)
滑开直接删 + 显示 undo toast:
private async deleteItem(id: number): Promise<void> {
const deleted = this.items.find(i => i.id === id)
if (!deleted) return
this.items = this.items.filter(i => i.id !== id)
await this.dao.delete(id)
// 显示 undo
promptAction.showToast({
message: '已删除。点此撤销',
duration: 5000,
// 鸿蒙 Toast 不支持 action button,用 Snackbar 替代
})
this.lastDeleted = deleted
this.lastDeletedTime = Date.now()
}
private async undoLast(): Promise<void> {
if (!this.lastDeleted) return
if (Date.now() - this.lastDeletedTime > 5000) return // 5 秒内才能撤销
await this.dao.insert(this.lastDeleted)
this.items = await this.dao.getAll()
this.lastDeleted = null
}
Snackbar / 自定义 Dialog 带按钮。鸿蒙的内置 Toast 没按钮,要自画。
撤销窗口 5 秒:平衡。
- 2 秒:用户反应不过来
- 10 秒:太长,用户已经走了
八、空列表的 Empty State
if (this.items.length === 0) {
Column() {
Image($r('app.media.ic_empty')).width(80).height(80)
Text('还没有数据').fontSize(14).fontColor('#999')
Button('添加第一条').onClick(() => this.openAddSheet())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else {
List({ space: 10 }) { ... }
}
空列表要有引导:
- 图标 + 文案 + 按钮
- 用户知道要去哪里添加
不要:只显示白屏。用户以为 app 坏了。
踩坑清单
- 侧滑用
swipeAction({ start, end }) - actionAreaDistance 100 vp 手感最好
- 红色 = 删除,蓝色 = 归档,约定俗成
- 删除要整数组赋值(or filter),不要 splice
- ForEach 的 key 必须指定
- 加淡出动画提升质感
- 重要数据要确认 dialog
- 轻量数据加 undo toast
- 空列表要有 引导(图标 + 文案 + 按钮)
尾声
侧滑交互 是列表 app 的标准动作。做好这一个交互 → 用户觉得"像原生 iOS Mail"(高质感);做不好 → 用户觉得"像半成品"。
独立开发者的建议:
- 所有 List 页都加侧滑(哪怕只有删除)
- 配 删除动画
- 轻量数据加 undo
- 空列表加 引导
对标:
- iOS Mail:侧滑归档 + 侧滑删除 的鼻祖
- Gmail:类似 pattern
- 鸿蒙 ArkUI:内置了这个 API,比 Android 的 ItemTouchHelper 省心
鸿蒙的 List 比 iOS 的 UITableView 更现代(声明式)。独立开发者用 ArkUI 的 List 开发速度比 iOS 快 30-50%。