当前位置: 首页 > news >正文

【HarmonyOS6】列表滑动操作实战

前言

在移动应用开发中,列表是最常见的 UI 组件之一。通过左滑或右滑,用户可以快速执行删除、编辑、收藏等操作,无需进入详情页面。

本文将通过一个实际案例——饮水记录列表,带你深入理解 SwipeAction 的使用方法和交互设计技巧。

什么是 SwipeAction

SwipeAction(滑动操作)是一种常见的移动端交互模式。用户在列表项上左滑或右滑,会显示隐藏的操作按钮,点击按钮执行相应的操作。

案例背景

我们要实现一个饮水记录列表,具备以下功能:

  1. 显示每条饮水记录的时间和饮水量
  2. 左滑显示删除按钮
  3. 点击删除按钮移除记录
  4. 删除后列表自动更新
  5. 提供流畅的滑动动画效果

最终效果如下图所示:

完整代码

首先,让我们看一下完整的实现代码:

import{PreferencesService,getTodayDateString}from'../service/PreferencesService';import{WaterRecord,DailyWaterSummary}from'../model/HealthModels';import{CommonCard}from'../components/CommonCard';importcommonfrom'@ohos.app.ability.common';@Entry@Componentstruct WaterPage{@StatewaterCurrent:number=0;@StatewaterTarget:number=2000;@StatewaterRecords:WaterRecord[]=[];privateprefService:PreferencesService|null=null;aboutToAppear():void{constctx=getContext(this)ascommon.UIAbilityContext;this.prefService=PreferencesService.getInstance(ctx);this.loadWaterData();}asyncloadWaterData():Promise<void>{if(!this.prefService)return;consttoday=getTodayDateString();constdata=awaitthis.prefService.getWaterData(today);this.waterCurrent=data.totalAmount;this.waterTarget=data.targetAmount;this.waterRecords=data.records;}asyncremoveRecord(recordId:number):Promise<void>{if(!this.prefService)return;consttoday=getTodayDateString();awaitthis.prefService.removeWaterRecord(today,recordId);awaitthis.loadWaterData();}@BuilderDeleteButton(recordId:number){Row(){Text('删除').fontSize(14).fontColor(Color.White)}.width(70).height('100%').justifyContent(FlexAlign.Center).backgroundColor($r('app.color.error_color')).onClick(()=>{this.removeRecord(recordId);})}build(){Scroll(){Column(){// 今日记录Column(){CommonCard({title:'今日记录'}){Column(){if(this.waterRecords.length===0){Text('暂无记录').fontSize(14).fontColor($r('app.color.text_secondary')).padding({top:16,bottom:16})}else{List(){ForEach(this.waterRecords.slice().reverse(),(record:WaterRecord)=>{ListItem(){Row(){Text('💧').fontSize(18)Text(`${record.amount}ml`).fontSize(16).fontColor($r('app.color.text_primary')).margin({left:8})Blank()Text(record.time).fontSize(14).fontColor($r('app.color.text_secondary'))}.width('100%').padding({top:12,bottom:12})}.swipeAction({end:this.DeleteButton(record.id)})})}.width('100%').divider({strokeWidth:1,color:$r('app.color.divider_color'),startMargin:0,endMargin:0})}}}}.margin({left:16,right:16,bottom:32})}}.width('100%').height('100%').backgroundColor($r('app.color.background_color'))}}

逐步解析

1. 数据模型定义

在开始实现 UI 之前,我们需要先了解数据结构:

// 单条饮水记录interfaceWaterRecord{id:number;// 唯一标识amount:number;// 饮水量(毫升)time:string;// 记录时间 HH:mmdate:string;// 记录日期 YYYY-MM-DD}// 每日饮水汇总interfaceDailyWaterSummary{date:string;// 日期totalAmount:number;// 总饮水量targetAmount:number;// 目标饮水量records:WaterRecord[];// 饮水记录列表}

数据结构设计要点:

  1. 唯一标识 id: 每条记录都有唯一的 id,用于删除操作时精确定位
  2. 时间分离: date 和 time 分开存储,便于按日期查询和按时间排序
  3. 汇总数据: DailyWaterSummary 包含统计信息和详细记录,避免重复计算

为什么需要 id?

考虑这样的场景:用户在同一时间添加了两条 200ml 的记录。如果没有 id,我们无法区分要删除哪一条。所以id是必不可少的,这样可以方便我们精确地去删除某一条记录。

2. 组件状态管理

@StatewaterCurrent:number=0;@StatewaterTarget:number=2000;@StatewaterRecords:WaterRecord[]=[];

状态变量详解:

waterCurrent: 当前饮水总量

  • 类型:number
  • 用途: 显示今日已喝水量,计算完成度
  • 更新时机: 添加或删除记录后重新计算

waterTarget: 目标饮水量

  • 类型:number
  • 用途: 显示目标值,计算完成百分比
  • 更新时机: 用户修改设置时更新

waterRecords: 饮水记录列表

  • 类型:WaterRecord[]
  • 用途: 渲染列表,展示所有记录
  • 更新时机: 加载数据、添加记录、删除记录后更新

为什么使用 @State?

@State装饰器让变量具有响应式特性。当变量值改变时,使用该变量的 UI 会自动重新渲染:

this.waterRecords=newRecords;

如果不使用@State,修改变量后 UI 不会更新,用户看不到变化。

3. 数据加载

privateprefService:PreferencesService|null=null;aboutToAppear():void{constctx=getContext(this)ascommon.UIAbilityContext;this.prefService=PreferencesService.getInstance(ctx);this.loadWaterData();}asyncloadWaterData():Promise<void>{if(!this.prefService)return;consttoday=getTodayDateString();constdata=awaitthis.prefService.getWaterData(today);this.waterCurrent=data.totalAmount;this.waterTarget=data.targetAmount;this.waterRecords=data.records;}

生命周期方法 aboutToAppear:

aboutToAppear()是组件的生命周期方法,在组件即将显示时调用。这是初始化数据的最佳时机。

为什么使用 async/await?

数据加载是异步操作(需要读取本地存储),使用async/await可以让代码更清晰:

// 使用 async/await: 代码清晰,易读asyncloadWaterData():Promise<void>{constdata=awaitthis.prefService.getWaterData(today);this.waterRecords=data.records;}

空值检查的重要性:

if(!this.prefService)return;

这行代码防止prefServicenull时调用方法导致崩溃。虽然正常情况下不会为null,但防御性编程是好习惯。

4. 删除操作实现

asyncremoveRecord(recordId:number):Promise<void>{if(!this.prefService)return;consttoday=getTodayDateString();awaitthis.prefService.removeWaterRecord(today,recordId);awaitthis.loadWaterData();}

为什么要重新加载数据?

删除记录后,我们需要更新三个数据:

  1. waterRecords: 记录列表
  2. waterCurrent: 当前饮水总量(需要重新计算)
  3. 本地存储: 持久化删除操作

直接调用loadWaterData()可以一次性更新所有数据,保证数据一致性:

异步操作的顺序:

注意await的使用顺序:

awaitthis.prefService.removeWaterRecord(today,recordId);// 先删除awaitthis.loadWaterData();// 再加载

必须等删除操作完成后,再加载数据。如果不用await,可能出现数据不一致:

// ❌ 错误示例: 不等待删除完成this.prefService.removeWaterRecord(today,recordId);// 开始删除(异步)awaitthis.loadWaterData();// 立即加载(删除可能还没完成)// 结果: 加载的数据中还包含刚删除的记录

5. 删除按钮的设计

@BuilderDeleteButton(recordId:number){Row(){Text('删除').fontSize(14).fontColor(Color.White)}.width(70).height('100%').justifyContent(FlexAlign.Center).backgroundColor($r('app.color.error_color')).onClick(()=>{this.removeRecord(recordId);})}

使用 @Builder 的原因:

@Builder装饰器用于封装可复用的 UI 片段。删除按钮会在每个列表项中使用,封装成@Builder有以下好处:

  1. 代码复用: 避免在每个 ListItem 中重复编写相同的代码
  2. 易于维护: 修改按钮样式时,只需修改一处
  3. 参数化: 可以传入不同的 recordId,实现不同的删除逻辑

点击事件处理:

.onClick(()=>{this.removeRecord(recordId);})

点击按钮时,调用removeRecord()方法,传入当前记录的 id。这样每个删除按钮都知道要删除哪条记录。

6. 列表结构实现

List(){ForEach(this.waterRecords.slice().reverse(),(record:WaterRecord)=>{ListItem(){Row(){Text('💧').fontSize(18)Text(`${record.amount}ml`).fontSize(16).fontColor($r('app.color.text_primary')).margin({left:8})Blank()Text(record.time).fontSize(14).fontColor($r('app.color.text_secondary'))}.width('100%').padding({top:12,bottom:12})}.swipeAction({end:this.DeleteButton(record.id)})})}.width('100%').divider({strokeWidth:1,color:$r('app.color.divider_color'),startMargin:0,endMargin:0})

List 组件:

List是 ArkTS 提供的列表容器组件,专门用于渲染列表数据。它的特点:

  1. 高性能: 支持虚拟滚动,只渲染可见区域的列表项
  2. 丰富的功能: 支持分组、索引、滑动操作等
  3. 自动优化: 自动管理列表项的创建和销毁

ForEach 循环渲染:

ForEach(this.waterRecords.slice().reverse(),(record:WaterRecord)=>{// 渲染每一项})

为什么使用 slice().reverse()?

this.waterRecords.slice().reverse()

这行代码包含两个操作:

  1. slice(): 创建数组的浅拷贝

    • 原因:reverse()会修改原数组
    • 如果直接this.waterRecords.reverse(),会改变原始数据
    • 使用slice()创建副本,保护原始数据
  2. reverse(): 反转数组顺序

    • 原因: 数据按时间正序存储(早→晚)
    • 显示时需要倒序(晚→早),最新的记录在最上面
    • 符合用户习惯(最新的信息最重要)

如下图所示:

Blank() 组件的作用:

Row(){Text('💧')Text(`${record.amount}ml`)Blank()// 占据剩余空间Text(record.time)}

Blank()是一个弹性空白组件,会占据 Row 中的所有剩余空间。

divider 分割线:

.divider({strokeWidth:1,color:$r('app.color.divider_color'),startMargin:0,endMargin:0})

divider在列表项之间添加分割线,参数说明:

  • strokeWidth: 线条粗细(1px)
  • color: 线条颜色(通常是浅灰色)
  • startMargin: 左侧边距(0 表示从最左边开始)
  • endMargin: 右侧边距(0 表示到最右边结束)

7. SwipeAction 核心实现

ListItem(){Row(){// 列表项内容}}.swipeAction({end:this.DeleteButton(record.id)})

swipeAction 参数详解:

swipeActionListItem的一个方法,用于配置滑动操作。它接受一个对象参数:

interfaceSwipeActionOptions{start?:CustomBuilder;// 右滑显示的内容end?:CustomBuilder;// 左滑显示的内容}

start 和 end的对比:

  • start: 从左向右滑动时显示(右滑)

    • 通常用于"标记已读"、"收藏"等非破坏性操作
    • 显示在列表项左侧
  • end: 从右向左滑动时显示(左滑)

    • 通常用于"删除"、"移除"等破坏性操作
    • 显示在列表项右侧

为什么删除操作使用 end?

这是移动端的通用设计规范,大多数应用都是这样设计的。

多个操作按钮:

如果需要多个操作按钮,可以使用Row组合:

@BuilderMultipleActions(recordId:number){Row(){// 编辑按钮Row(){Text('编辑').fontSize(14).fontColor(Color.White)}.width(70).height('100%').justifyContent(FlexAlign.Center).backgroundColor('#FF9800').onClick(()=>{this.editRecord(recordId);})// 删除按钮Row(){Text('删除').fontSize(14).fontColor(Color.White)}.width(70).height('100%').justifyContent(FlexAlign.Center).backgroundColor('#F44336').onClick(()=>{this.removeRecord(recordId);})}}// 使用ListItem(){// ...}.swipeAction({end:this.MultipleActions(record.id)})

效果:

8. 空状态处理

if(this.waterRecords.length===0){Text('暂无记录').fontSize(14).fontColor($r('app.color.text_secondary')).padding({top:16,bottom:16})}else{List(){// 列表内容}}

为什么需要空状态?

当用户第一次使用应用,或删除了所有记录后,列表为空。如果不处理空状态,用户会看到一片空白,不知道发生了什么。

交互设计的实践

1. 滑动方向的选择

左滑(end) vs 右滑(start):

操作类型推荐方向原因
删除左滑(end)符合用户习惯,大多数应用都是左滑删除
编辑左滑(end)与删除一起出现时,放在删除左侧
收藏右滑(start)正向操作,与删除区分开
标记已读右滑(start)非破坏性操作,使用右滑更安全
归档左滑(end)类似删除的移除操作

以上只是一些简单的建议,可以综合考虑实际需求来确定是左滑还是右滑

总结

通过本文的学习,我们深入了解了 SwipeAction 的使用方法和交互设计技巧。也通过了一个实际的案例来实践。

http://www.jsqmd.com/news/1244239/

相关文章:

  • SpringBoot学习之直接使用枚举Enum(基于Mybatis-Plus)(五十一)
  • Java 掉到第四了?慌了
  • 2026年7月最新积家北京长安商场维修保养服务电话 - 积家官方售后服务中心
  • 江诗丹顿长沙****网点地址与售后客户热线2026年7月最新通告 - 江诗丹顿服务中心
  • 公司有了ERP,为什么老板还要安装MES?它们的区别在哪?
  • 多步骤智能体任务断层?Kimi-K3 前沿推理专用模型,DMXAPI对接300多款API,AIAgent 开发必备
  • 伯爵**售后服务中心网点地址及服务热线实地考察报告_多信源验证(2026年7月最新) - 亨得利官方服务中心
  • 百达翡丽**售后维修收费咨询|2026 年 7 月**维修服务中心地址热线 - 百达翡丽官方服务中心
  • 从入门到精通:Node API for .NET核心功能详解与实战案例
  • 用 Cursor + PM Skills,把「想法」一次做到「可点原型」
  • 实测照片数据揭示:屏幕空间环境光遮蔽(SSAO)渲染效果是否失真?
  • 终极指南:OpenSpeedy如何通过Ring3 Hook技术实现游戏时间函数拦截与帧率控制优化
  • GraphPipe API完全参考:轻松调用机器学习模型服务
  • PromptFoo 源码分析与工程实战:LLM 测试框架的架构与最佳实践
  • 嵌入式+FreeRTOS移植控制实现
  • 苏州 7 大区域黄金回收全整理!典籍黄金回收门店就近变现不踩坑 - 典籍黄金回收中心
  • 如何提高测试用例覆盖率
  • 【图解算法】回溯法核心思想与 Java 实战(中低难度力扣题)
  • 2026 年 7 月百达翡丽国内**售后渠道|正规维修服务中心地址与热线 - 百达翡丽官方服务中心
  • 羽球搭子 HarmonyOS 实战(16):实时计分页的状态机设计
  • Windows系统文件dwmscene.dll丢失找不到问题解决
  • 分布式 ID
  • 2026年工商业储能系统推荐:系统效率、循环寿命与安全认证全解析 - 科技焦点
  • AI写作金句植入的5个致命误区:92%的创作者正在 silently 毁掉爆款基因
  • 市场知名的测功机工厂
  • gh_mirrors/log/logback授权控制详解:基于角色与权限的访问管理
  • C语言控制语句-循环
  • 如何用Niva快速构建第一个跨平台桌面应用?5分钟上手教程
  • 2026年7月博世壁挂炉售后服务电话24小时全新专属热线升级公示最新公告 - 家电技术百科
  • Linux下MyIpAdd库的使用