HarmonyOS6.1 列表项滑动删除实战 — 从微信左滑学swipeAction

📅 2026/7/10 22:27:03
HarmonyOS6.1 列表项滑动删除实战 — 从微信左滑学swipeAction
微信聊天列表左滑出标为已读“置顶”“删除”这个交互从iOS时代沿用至今几乎成了用户对列表操作的肌肉记忆。HarmonyOS NEXT的ArkUI提供了swipeAction API理论上一行代码就能实现左滑菜单但我第一次用的时候踩了一堆坑——按钮高度对不上、删除后列表不刷新、swipeAction和拖拽排序打架……这篇就把这些实战中的细节掰开了说。swipeAction的基本用法左滑出操作按钮swipeAction是ListItem的专属方法只能在ListItem内部调用。它的参数结构是{ start, end }——start对应右滑时左侧露出的按钮区end对应左滑时右侧露出的按钮区。微信那种左滑删除用的就是end。来看SwipeActionDemo里的核心代码interfaceSwipeItem{id:number;title:string;desc:string;time:string;}List({space:8}){ForEach(this.items,(item:SwipeItem){ListItem(){Row(){Column(){Text(item.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor(#333333)Text(item.desc).fontSize(12).fontColor(#888888).margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text(item.time).fontSize(11).fontColor(#bbbbbb)}.width(100%).padding(14).backgroundColor(#ffffff).borderRadius(8)}.swipeAction({end:{builder:():void{this.swipeEndBuilder(item);}}})},(item:SwipeItem)item.id.toString())}注意几个关键点第一swipeAction的builder参数类型是() void不是直接传Builder函数名。这里用的是() { this.swipeEndBuilder(item); }这种包装写法因为需要把当前item传进去。如果你直接写builder: this.swipeEndBuilder不传参数是OK的但一旦要传参就得用箭头函数包一层。第二ForEach的key generator用了item.id.toString()。swipeAction场景下key特别重要——如果key不稳定比如用index做key删除某一项后列表重新渲染时可能导致滑动状态错乱已经展开的按钮跑到别的行上去。第三swipeAction只认ListItem。你有其他容器比如Row、Column想加swipeAction不行。必须套在ListItem里而ListItem又必须在List里。这个层级关系不能打乱。滑出按钮的设计高度必须和列表项一致swipeEndBuilder的实现如下BuilderswipeEndBuilder(item:SwipeItem):void{Row(){Button(已读).fontSize(12).height(60).backgroundColor(#1a73e8).fontColor(Color.White).borderRadius(0).onClick((){this.readItem(item.id);})Button(置顶).fontSize(12).height(60).backgroundColor(#e65100).fontColor(Color.White).borderRadius(0).onClick((){this.pinItem(item.id);})Button(删除).fontSize(12).height(60).backgroundColor(#d32f2f).fontColor(Color.White).borderRadius({topRight:8,bottomRight:8}).onClick((){this.deleteItem(item.id);})}}这里有个很关键的细节——Button的height设成了60。为什么是60因为列表项的主内容Rowpadding是14里面文字大概32像素高整体算下来约60。如果按钮高度和列表项高度不一致左滑出来的按钮区要么矮一截底部留白要么撑出多余空间看起来像bug。在实际项目中列表项高度往往是动态的内容行数不固定这时候滑动按钮的高度也要跟着动态走。一种做法是给ListItem和swipeAction的builder用同一个固定高度变量另一种是给按钮设height(100%)让它撑满——但这在ArkUI里不一定按预期工作因为swipeAction的builder所在的布局容器高度默认不会自动匹配ListItem内容高度。我踩过的一个坑列表项用了自适应高度没设固定height滑动按钮设了height(60)结果某些长内容的列表项左滑出来按钮只占了一半高底部一大段空白。最后只能给ListItem加了固定高度约束滑动按钮也用相同值。删除逻辑必须filter后重新赋值deleteItem的实现privatedeleteItem(id:number):void{constnewItems:SwipeItem[]this.items.filter((item:SwipeItem)item.id!id);this.itemsnewItems;this.addLog(删除: idid.toString());}这段代码看似简单但藏着ArkTS状态管理的核心机制。你不能写this.items.splice(index, 1)——splice虽然修改了数组但ArkUI的State对数组内部的变动检测是有限的。splice删除了元素但this.items的引用没变框架可能不触发UI刷新。正确的做法是filter产生新数组然后重新赋值this.items newItems。赋值操作改变了引用State才能检测到变化并驱动UI更新。这个规则在ArkTS里是通用的凡是涉及数组的增删改都要返回新数组再赋值。push、splice、sort这些原地修改方法在State场景下都可能失灵。这不是bug是设计——ArkUI的响应式系统基于引用比较引用不变就不重渲染。有人可能会想那我用Observed和ObjectLink行不行理论上可以但对这种简单的列表数据来说filter重新赋值是最清晰最不容易出问题的方案别过度设计。下拉刷新Refresh组件和$$双向绑定SwipeActionDemo还演示了Refresh组件配合List实现下拉刷新StateisRefreshing:booleanfalse;Refresh({refreshing:$$this.isRefreshing}){List(){ForEach([刷新内容1,刷新内容2,刷新内容3,刷新内容4,刷新内容5],(item:string){ListItem(){Text(item).fontSize(14).fontColor(#333333).padding(16).width(100%)}},(item:string,index:number)index.toString())}.width(90%).height(200).divider({strokeWidth:1,color:#eeeeee})}.onRefreshing((){this.addLog(下拉刷新触发);setTimeout((){this.isRefreshingfalse;this.addLog(刷新完成);},1500);})这里有两个要点$$this.isRefreshing——这是ArkTS的双向绑定语法。Refresh组件需要在数据请求完成后关闭刷新指示器它通过这个双向绑定来修改isRefreshing的值。如果你写成‘refreshing:this.isRefreshing‘单向Refresh组件就无法自动关闭刷新动画你得手动调别的方法来停。而用‘双向绑定语法。Refresh组件需要在数据请求完成后关闭刷新指示器它通过这个双向绑定来修改isRefreshing的值。如果你写成{ refreshing: this.isRefreshing }单向Refresh组件就无法自动关闭刷新动画你得手动调别的方法来停。而用双向绑定语法。Refresh组件需要在数据请求完成后关闭刷新指示器它通过这个双向绑定来修改isRefreshing的值。如果你写成‘refreshing:this.isRefreshing‘单向Refresh组件就无法自动关闭刷新动画你得手动调别的方法来停。而用‘组件直接改你的State变量你改变量组件也能感知真正的双向通信。onRefreshing回调里用setTimeout模拟了1.5秒的网络请求。实际项目中这里应该是异步数据拉取拉完设this.isRefreshing false。如果你忘了设这个false刷新动画就永远转用户只能杀进程。还有一个容易被忽略的点Refresh的子组件必须是可滚动的组件List、Scroll、Grid等如果放个Column进去下拉手势不会被识别。swipeAction和拖拽排序的冲突这是一个真实踩过的坑。项目需求是列表既支持左滑删除又支持长按拖拽排序。swipeAction和ListItem的dragSort组合使用时手势冲突严重——左滑时可能触发拖拽拖拽时可能左滑出按钮用户体验一塌糊涂。目前的ArkUI版本没有内置的手势优先级解决方案。我最终的做法是二选一——保留左滑删除用按钮触发排序上移/下移而不是拖拽。如果你的业务确实需要两个共存可以考虑用模式切换编辑模式下禁用swipeAction开启拖拽普通模式下启用swipeAction禁用拖拽。坑点汇总swipeAction只能用于ListItem内— 不是List不是Row是ListItem。ListItem又必须在List里。层级不对直接不生效也不会报错就是左滑没反应。删除后数组必须重新赋值— filter返回新数组赋值给State变量才能触发刷新。splice/push等原地修改方法不靠谱。滑动按钮高度要和列表项一致— 否则视觉上会出bug按钮区要么矮一截要么撑出空白。动态高度列表需要特殊处理。swipeAction和拖拽排序同时使用会冲突— 手势打架目前没有优雅解法建议用模式切换或改为按钮操作。Refresh的refreshing需要$$双向绑定— 单向绑定的话刷新动画无法自动停止。ForEach的key必须稳定— 用唯一id做key不要用index。删除操作后index会变导致滑动状态错乱。写在最后左滑删除看着简单真正做好需要抠每个像素对齐和每个状态转换swipeAction的坑远比API文档暗示的多。