# HarmonyOS ArkTS 滑动删除列表实战(三):交互式列表增删操作

摘要:列表滑动删除是移动应用中极为常见的交互模式。本文详细讲解如何在 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 组件是高性能列表容器,配合 ListItemForEach 实现数据驱动渲染:

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
    }
  }
}

滑动逻辑详解

  1. TouchType.Down:记录触摸起始位置
  2. TouchType.Move :计算手指位移,更新 offsetX,限制范围在 [-DELETE_BTN_WIDTH, 0]
  3. TouchType.Up :判断是否超过阈值 SWIPE_THRESHOLD(60vp)
    • 超过:吸附到删除按钮完全显示位置(-DELETE_BTN_WIDTH
    • 未超过:弹性回弹到原始位置(0
  4. 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 交互反馈设计

  1. 视觉反馈:滑动时列表项跟随手指移动,提供实时位置反馈
  2. 触感反馈 :删除时调用震动 API vibrator.vibrate(30)
  3. Toast 提示:操作完成后显示轻量提示
  4. 回弹动画:未达到阈值时弹性回弹,模拟真实物理效果

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 性能优化

  1. key 属性 :为 ForEach 提供唯一 key,提升 diff 效率

    arkts 复制代码
    ForEach(this.items, (item: ListItemData) => {
      ListItem() { ... }
    }, (item: ListItemData) => item.id.toString())
  2. 避免全量重建:数组操作后使用浅拷贝触发更新,而非重新创建整个数组

  3. LazyForEach :大量数据时使用 LazyForEach 进行虚拟列表渲染

6.2 错误处理

  1. 边界情况:空列表删除、删除最后一个项目、快速连续删除
  2. 同步状态:确保删除后列表索引与数据状态一致
  3. 异常恢复:删除失败时恢复数据

6.3 无障碍访问

  1. 为删除按钮添加 accessibilityText 描述
  2. 确保滑动操作有替代的按钮操作方式
  3. 使用 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]
  }
}

八、结语

滑动删除列表是移动应用中最常见也最实用的交互模式之一。通过本项目的学习,我们掌握了:

  1. List + ForEach 构建数据驱动列表
  2. onTouch 事件实现手势滑动检测
  3. translate 属性实现内容位移
  4. Toggle 组件切换操作模式
  5. 状态管理在增删操作中的应用

这些技能在笔记应用、任务管理、消息列表、购物车等场景中广泛适用,是 ArkTS 开发者的必会技能。

相关推荐
GitCode官方3 小时前
openPangu-2.0-Pro 模型及技术报告正式开源上线 AtomGit AI
人工智能·华为·开源·harmonyos·atomgit
想你依然心痛5 小时前
HarmonyOS 5.0智慧农业开发实战:构建分布式农业物联网与区块链农产品溯源系统
人工智能·分布式·物联网·区块链·智慧农业·harmonyos·开发实战
Kevin Wang7275 小时前
华为昇腾910B部署手册——课堂质量诊断
人工智能·华为
刹那芳华19927 小时前
STMF+ESP-S+MQTT协议连接华为云端(附踩坑记录)
java·struts·华为
想你依然心痛18 小时前
ArkTS 布局系统概述——从线性到网格,掌握声明式 UI 的骨架艺术
harmonyos·arkts·flex弹性布局·声明式布局·grid网格布局·响应式适配·布局性能优化
程序员黑豆21 小时前
鸿蒙应用开发:@Computed 装饰器详解与实战
前端·harmonyos
ldsweet21 小时前
HarmonyOS NEXT 音频播放器开发:AVPlayer 封装、播放列表与后台播放实战
华为·音视频·harmonyos
qizayaoshuap1 天前
# 44号应用:标签管理 — Flex 流式标签与交互状态设计
华为·harmonyos
2601_960906721 天前
华为MateBook Pro S首发搭载麒麟XE90
华为·postgresql·sqlite·时序数据库·tdengine