鸿蒙ArkUI实战:滑动操作与列表交互
2026/6/5 16:18:09 网站建设 项目流程

滑动操作是移动端列表交互的核心模式。本文用 ArkUI 的swipeActionAPI 构建一个完整的滑动操作列表——左滑删除、右滑置顶,覆盖双方向滑动、滑动阈值、操作确认以及空状态反馈。


一、我们要做什么

一个待办事项列表,每条支持两个方向的滑动操作:

  1. 左滑删除— 从右向左滑动,露出红色"删除"按钮。滑过阈值(80vp)自动触发删除,也可轻点按钮删除。删除后 Toast 反馈。
  2. 右滑置顶— 从左向右滑动,露出蓝色"置顶"按钮。已置顶的项(列表第一条)不再显示右滑按钮——避免无意义的重复置顶。
  3. 空状态— 删除全部 12 条后,显示空状态提示。
  4. 底部操作提示— “← 左滑删除 | 右滑置顶 →”,引导用户发现滑动手势。

二、数据模型

classSwipeItem{id:number;title:string;subtitle:string;time:string;constructor(id:number,title:string,subtitle:string,time:string){this.id=id;this.title=title;this.subtitle=subtitle;this.time=time;}}

12 条模拟数据,模拟一个开发者的待办清单——标题是主要任务,副标题是描述/备注,时间是最后修改时间。


三、swipeAction API 全解析

3.1 基本结构

ListItem(){// 列表项的正文内容}.swipeAction({start:{// 右滑(从左侧露出按钮)builder:()=>{this.pinButton()},onAction:()=>{this.pinItem(item.id)},actionAreaDistance:70},end:{// 左滑(从右侧露出按钮)builder:()=>{this.deleteButton()},onAction:()=>{this.deleteItem(item.id,item.title)},actionAreaDistance:80}})

swipeAction接收一个配置对象,包含两个可选字段:

字段方向按钮出现位置
start从左向右滑列表项左侧露出按钮
end从右向左滑列表项右侧露出按钮

3.2 三个关键字段

每个方向的配置对象包含:

字段类型说明
builderCustomBuilder滑动后露出的按钮 UI,必须是() => void类型的 Builder
onAction() => void滑动距离超过阈值时的回调。触发后自动收起滑动状态
actionAreaDistancenumber(vp)阈值距离。滑过这个距离松手 →onAction触发;未滑过 → 按钮弹回

三个执行路径:

  1. 滑过阈值松手onAction触发(自动执行操作)
  2. 轻滑后松手(按钮露出但未过阈值)→ 按钮保持在露出状态,用户可轻点按钮
  3. 轻点露出的按钮→ 按钮的onClick触发

onAction的语义是"滑动到位,执行操作"——它替代了按钮的 onClick。所以通常做法是:builder 只负责视觉,onAction 负责逻辑,按钮的 onClick 可以省略。

3.3 为什么 builder 不能直接传 @Builder 引用?

builder字段的类型是CustomBuilder,定义为() => void。你不能写builder: this.pinButton,因为 ArkTS 不支持直接传 @Builder 方法引用。必须用箭头函数包裹:builder: () => { this.pinButton() }

这个箭头函数也解决了闭包捕获问题——在 ForEach 内部,箭头函数捕获当前循环的item,每个 ListItem 的 swipeAction 持有对各自 item 的正确引用。


四、交互点1:左滑删除

4.1 删除按钮 Builder

@BuilderdeleteButton(){Row(){Text('删除').fontSize(FontSize.BODY).fontColor(Color.White)}.width(80).height('100%').backgroundColor(AppColors.ERROR).justifyContent(FlexAlign.Center)}

关键细节:.height('100%')让删除按钮自动撑满 ListItem 的高度。不需要写死高度——List 的每个 ListItem 高度可能不同,百分比高度让按钮与内容等高,视觉效果干净统一。

红色背景AppColors.ERROR (#FF4D4F)是删除操作的通用颜色——用户看到红色就知道是"破坏性操作"。

4.2 删除逻辑

privatedeleteItem(id:number,title:string):void{constidx=this.items.findIndex((item:SwipeItem)=>item.id===id);if(idx>=0){this.items.splice(idx,1);this.items=[...this.items];promptAction.showToast({message:`已删除:${title}`,duration:1500});}}

Toast 包含了被删条目的标题——让用户知道删了什么,比"已删除"三个字更有信息量。1500ms 的 duration 足够阅读。

4.3 actionAreaDistance = 80

为什么设 80vp?ArkUI 默认的actionAreaDistance是 56vp。设得更大(80vp)意味着用户需要滑得更远才会触发自动执行——降低误操作概率。删除是破坏性操作,不应该太容易触发。

置顶的actionAreaDistance设 70vp——比删除略低,因为置顶不是破坏性操作,用户不慎触发也容易恢复。


五、交互点2:右滑置顶

5.1 置顶按钮的条件显示

start:this.items[0]?.id!==item.id?{builder:()=>{this.pinButton()},onAction:()=>{this.pinItem(item.id)},actionAreaDistance:70}:undefined,

this.items[0]?.id !== item.id判断当前项是否已经是第一条。如果是 →start: undefined→ 右滑无操作。如果不是第一条 →start: { builder, onAction, actionAreaDistance }→ 右滑可置顶。

这个条件判断避免了"第一条也能置顶"的无意义操作——已经是第一条了,再置顶等于没动。

5.2 置顶逻辑

privatepinItem(id:number):void{constidx=this.items.findIndex((item:SwipeItem)=>item.id===id);if(idx>0){constitem=this.items[idx];this.items.splice(idx,1);this.items=[item,...this.items];this.items=[...this.items];promptAction.showToast({message:'已置顶',duration:1200});}}

三步:

  1. splice(idx, 1)从原位置取出
  2. [item, ...this.items]插入到数组头部
  3. [...this.items]触发 @State 更新

注意idx > 0而非idx >= 0——idx 为 0 时已在顶部,不操作。


六、交互点3:滑动阈值与松手回弹

actionAreaDistance定义了"临界点"。用户滑动时有两种松手结果:

滑过 80vp → 松手 → onAction 触发 → 删除 → Toast 未过 80vp → 松手 → 按钮保持露出状态 → 用户可轻点按钮或右滑收回

在实际测试中,用户可以:

  1. 轻滑露出按钮 → 看一眼 → 决定不删 → 反方向滑回 → 按钮隐藏
  2. 轻滑露出按钮 → 点击红色"删除"文字 → 删除
  3. 用力滑过 80vp → 松手即删 → 最快路径

这给了用户"分层次的操作自由度"——这是好的手势设计。


七、交互点4:空状态

if(this.items.length===0){Column(){Image($r('sys.symbol.archivebox')).width(48).height(48).fillColor(AppColors.TEXT_DISABLED).margin({bottom:Spacing.MD})Text('列表为空').fontSize(FontSize.BODY).fontColor(AppColors.TEXT_DISABLED)Text('左右滑动列表项进行操作').fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_DISABLED).margin({top:Spacing.XS})}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)}

sys.symbol.archivebox(已验证可用的符号)作为空状态图标。两条提示文字——"列表为空"是当前状态,"左右滑动列表项进行操作"是操作引导。在空状态下这条引导没有实际作用,但它强化了用户对滑动手势的认知——下次有数据的时候就知道可以滑了。


八、列表项内容布局

Row(){Column(){Text(item.title).fontSize(FontSize.BODY).fontColor(AppColors.TEXT_PRIMARY).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})Text(item.subtitle).fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_TERTIARY).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).margin({top:2})}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text(item.time).fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_DISABLED).margin({left:Spacing.SM})}.width('100%').padding({left:Spacing.LG,right:Spacing.LG,top:Spacing.MD,bottom:Spacing.MD}).backgroundColor(Color.White)

双行文本 + 右侧时间,和微信消息列表布局一致。layoutWeight(1)让标题区域占据剩余宽度,时间文字靠右对齐。两个maxLines(1) + textOverflow防止长文本溢出。


九、底部操作提示

Text('← 左滑删除 | 右滑置顶 →').fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_DISABLED).width('100%').textAlign(TextAlign.Center).padding({top:Spacing.SM,bottom:Spacing.SM}).backgroundColor(AppColors.BACKGROUND)

这条提示固定在列表底部。左右箭头符号配合文字,直观告诉用户两个方向分别对应什么操作——滑动手势的发现性差,很多用户不知道可以滑,一行提示能大幅提升交互的曝光率。


十、页面结构总结

SwipePage (~190 行) ├── 数据层 │ ├── SwipeItem 类 (id, title, subtitle, time) │ └── MOCK_ITEMS 常量 (12 条) ├── 状态层 │ └── @State items: SwipeItem[] ├── 业务方法 │ ├── deleteItem(id, title) — 删除 + Toast │ └── pinItem(id) — 置顶 + Toast ├── 操作按钮 Builder │ ├── deleteButton() — 红色 80vp 宽 │ └── pinButton() — 蓝色 70vp 宽 └── UI 结构 ├── Header (返回 + 标题 + 计数) ├── List + ForEach (swipeAction: start + end) ├── 空状态 (archivebox 图标) └── 底部提示 (← 左滑删除 | 右滑置顶 →)

十一、常见面试题 / 踩坑点

11.1actionAreaDistance设多少合适?

  • 删除等破坏性操作— 60-80vp,稍长,减少误操作
  • 收藏/标记等非破坏性操作— 40-60vp,快速触发
  • 默认值— 56vp
  • 不要设到 100vp 以上— 用户滑不到,等于这个功能不存在

11.2 为什么用findIndex + splice而不用filter

两种写法的差异:

// splice 方案constidx=this.items.findIndex(m=>m.id===id);this.items.splice(idx,1);this.items=[...this.items];// filter 方案this.items=this.items.filter(m=>m.id!==id);

splice方案在删除后立即退出,O(n) 查找 + O(1) 删除。filter方案遍历整个数组创建新数组,O(n) 遍历 + O(n) 空间。对于 12 条数据差异可忽略,但对于 1000 条数据,splice只要找到目标就停止。

更重要的是,deleteItem需要拿到被删项的title用于 Toast 提示——splice方案在删除前已经拿到了引用,filter方案需要在外面再 find 一次。

11.3startend可以同时作用于一个 ListItem 吗?

可以,如本 Demo。但要注意:

  • 两个方向的滑动操作互斥——同时只能滑一个方向
  • 如果某个方向设为undefined,该方向不能滑动
  • 第一条的start: undefined就是利用了这个特性

11.4builder闭包中的item引用安全吗?

安全。ForEach 的每次迭代创建独立的闭包作用域,builder: () => { this.pinButton() }中的箭头函数捕获了外层 ForEach 回调中的item变量。由于itemconst(ForEach 的回调参数),不存在变量被后续迭代覆盖的问题。

11.5swipeAction对 ListItem 的性能有影响吗?

每个 ListItem 的 swipeAction 会创建额外的 DOM 节点(按钮区域)。对于 50 条以内的列表几乎无影响。如果需要优化 500+ 条的列表,建议配合LazyForEach使用,只渲染可见区域的 ListItem。

11.6 滑动手势和 List 的滚动冲突吗?

不冲突。ArkUI 的手势系统内部处理了水平滑动(swipeAction)和垂直滚动(List)的手势竞争——水平滑动优先触发 swipeAction,垂直滑动优先触发列表滚动。用户斜向滑动时,系统根据主要轴向判断意图。


十二、运行方式

代码位于dev/entry/src/main/ets/pages/SwipePage.ets

用 DevEco Studio 打开dev/项目,首页点击"滑动操作 — 左滑删除与右滑置顶"即可体验:

  1. 进入页面 → 12 条待办事项,底部提示"← 左滑删除 | 右滑置顶 →"
  2. 左滑任意条目 → 露出红色"删除"按钮;继续滑过 80vp 松手 → 自动删除 + Toast
  3. 左滑后松手(未过阈值)→ 按钮保持露出;轻点"删除"文字 → 同样删除
  4. 右滑非首条 → 露出蓝色"置顶"按钮 → 自动或手动触发 → 条目跳到列表第一
  5. 列表首条右滑无效 → 没有置顶按钮(已是最顶)
  6. 删除全部 12 条 → 空状态提示"列表为空"
  7. 查看顶部计数 → 实时显示剩余条目数

十三、扩展方向

  • 两段式滑动— 第一阶段露出单个按钮(70vp),第二阶段滑到 140vp 露出第二个按钮(如"删除 + 归档"),参考 iOS 邮件 App
  • 自定义操作菜单— 不止一个按钮,露出后显示 2-3 个操作(编辑、删除、分享),用 Row 布局
  • 撤销删除— 删除后不立即 splice,先"软删除"(灰色 + 划线),3 秒内可撤销。类似 Gmail 的撤销模式
  • 振动反馈— 滑过阈值时触发vibrator.vibrate(10),物理反馈增强操作感
  • 滑动选择模式— 长按进入多选模式,每个 ListItem 左侧出现 Checkbox,不再触发 swipeAction
  • 左滑操作记录— 左滑删除后写入"操作历史"数组,在页面外提供"最近删除"入口恢复
  • 弹性动画— 自定义 swipeAction 的回弹曲线,让松手后的回弹更自然
  • Dark Mode 适配— 操作按钮在暗黑模式下调整色彩饱和度,避免红色在暗底上刺眼

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询