做 Flutter 列表交互这么久,“侧滑删除 + 长按批量操作”一直是我绕不开的两个硬需求。这次在 OpenHarmony 上做仿原生系统风格列表,我以为直接把 Flutter 那套 Dismissible、ReorderableListView 拼一拼就能交差,结果真机一跑就露馅:手势判定跟 ArkUI 原生的侧滑差距太大,批量左滑菜单也容易误触,更别提动画节奏、阻尼、状态复位这些细节。于是花了几天时间把列表这一整套交互重新做了一遍深度定制,最终效果接近原生质感。这篇文章就把我的踩坑过程、状态设计思路和关键代码拆开来讲,希望对要在 OpenHarmony 上用 Flutter 做类似功能的同行有帮助。
1. 项目背景与定制目标
先说清楚,我这里说的“OpenHarmony 上的 Flutter”,指的是通过官方或社区维护的 Flutter-OHOS 适配分支,把 Flutter 工程跑在 OpenHarmony 真机上。由于 OpenHarmony 已经支持了比较完整的 Flutter 运行时,大部分 UI 逻辑可以跨端复用,但手势、滚动、列表项的交互细节跟 Android/iOS 生态仍然有差异,尤其在侧滑删除、批量操作这种强交互场景里,需要单独做深度定制。
1.1 为什么直接“拿来主义”不行
Flutter 官方组件里,跟侧滑删除最接近的是 Dismissible,跟拖拽排序相关的是 ReorderableListView。但它们解决的是“整行滑动后消失”和“长按拖动改变顺序”,跟原生系统列表的侧滑菜单完全是两回事。
原生风格列表通常需要满足几个体验点:
- 行内容左滑,露出后方的删除按钮,按钮可以是一个也可以是多个;
- 手指松手后,如果滑动距离不够,列表行自动回弹,而不是直接触发删除;
- 滑动过程中有一个最大滑动距离,超过后手指继续左移也不会把整行甩飞;
- 删除操作在确认后,行需要以合适的动画移除,同时保持列表滚动位置稳定;
- 长按列表项进入多选模式,此时侧滑手势失效,顶部或底部的批量操作栏出现。
这些需求用 Dismissible 实现非常别扭,因为 Dismissible 的核心逻辑是“滑过阈值就消失”,它不善于维护一个“菜单露出”状态;而批量操作则需要一个独立于单行滑动之外的全局选中状态。把这两套逻辑强行塞进一个通用组件里,代码会越来越难维护。
1.2 理想目标定成什么样
我给自己定的目标不是“代码能跑”,而是“还原原生交互的肌肉记忆”:
- 侧滑删除从触摸开始到菜单露出,动画时长、曲线、阻尼要跟系统原生接近;
- 同一时间只有一行可以处于侧滑展开状态,点击其他行或下滑其他行时当前展开行自动收起;
- 批量模式下,列表项头部出现选中圈,点击任意行切换选中态,顶部显眼位置显示已选数量或操作入口;
- 批量操作进行时,滑动手势不跟点击选择打架;
- 列表渲染性能在大量数据下依旧稳定,滑动过程不掉帧。
基于这些目标,我放弃组件拼装,选择自己实现核心的状态管理再组合基础组件,最终代码量虽然比用现成组件大,但每个行为都可控,后面的问题排查也变得容易。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路:从状态机到组件分工
这种交互定制,最容易翻车的地方在于“不知道当前处于什么状态”。用户的手指在屏幕上动一下,可能触发滑动、点击、滚动、长按好几个手势的竞争。如果一开始不把状态定义清楚,后面所有代码都会在 case 堆里打转。
2.1 列表行的状态怎么抽象
我做了个枚举,把所有可能的行状态列出来:
normal:普通状态,行内容完整展示;swiping:手指正在滑动,行内容跟着偏移;menuOpen:已经左滑露出操作按钮,手指离开但菜单保持打开;menuClosing:正在收起菜单;editMode:长按进入批量操作模式,此时行左侧出现选中圈;selected:在批量模式下,这一行被选中。
这几种状态之间并不是任意切换的。比如从 menuOpen 可以直接切换到 editMode,但必须先收起菜单;从 swiping 不能直接跳到 selected,因为批量模式的入口是长按,而长按需要手指先停留一段时间,此时行不应该处于滑动状态。
我把状态迁移画成了一张显式的关系表,然后严格按照迁移表写逻辑,能避免很多隐性 bug。
2.2 组件之间的分工方式
整个列表我拆成了三层:
ListScreen:负责整个页面,承载状态管理和批量工具栏;SlidableWidget:负责单行的手势识别、位移计算、菜单展示;RowContent:负责行的普通内容展示和
