做待办清单、歌单、收藏夹排序,第一反应往往是去翻 List 的 onItemDragStart、onItemDragMove、onItemDrop 这一套回调。文档里有、类型声明也在、编译不报错,但真机长按拖起来之后,三个回调一次都不进。这不是你的写法有问题,是这套旧 API 在 HarmonyOS 7 里已经走不通了。

旧路依赖 ListItem 的 editable 属性标记可编辑态,而这个属性从 API 9 起就已经废弃。长按起拖的事件被通用拖拽管线接管,日志里能看到 AceDrag 管线的启动记录,但 onItemDragStart 系列从头到尾沉默。你搜到的教程没有过期,只是文档断层让一条已经断掉的激活入口看起来仍然活着。

新路只有一条:ListItem 设 draggable(true) 并在 onDragStart 里塞数据,List 容器在 onDrop 里接收并落位。走通这条路的难点不在 API 调用,在一个几乎人人会撞的隐性错误——换位之后,起拖时那个下标已经过期了。

下标在数组搬移之后为什么会错位

假设九条待办,你把第 2 条拖到第 6 条的位置。onDrop 回调节点给你的 insertIndex 是落点位置,dragIndex 是起拖位置。直觉写法是 arr.splice(dragIndex, 1) 删掉源项,然后 arr.splice(insertIndex, 0, item) 插到落点。问题出在删除那一步:删掉源项之后,落点之后的所有元素都往前挪了一位,原来 insertIndex 指向的位置已经不再是用户松手时的那个位置。

正确的顺序不是“先删后插”,而是先判断方向,再修正落点。如果 dragIndex < insertIndex,删除源项后落点需要减一;如果 dragIndex > insertIndex,落点不变。

typescript

function moveItem<T>(arr: T[], dragIndex: number, insertIndex: number): T[] {
  const next = [...arr]
  const [moved] = next.splice(dragIndex, 1)
  const target = dragIndex < insertIndex ? insertIndex - 1 : insertIndex
  next.splice(target, 0, moved)
  return next
}

dragIndex < insertIndex 时 insertIndex - 1,这一行就是整个排序逻辑的核心。漏掉它,向下拖永远会少一位,向上拖看起来正常,导致你以为是“有时候对有时候不对”。这不是偶发 bug,是方向条件没覆盖全。

身份序列比下标可靠

下标错误修好之后,还有一个更隐蔽的问题:数据源刷新之后,下标本身可能不再是稳定的身份标识。列表如果同时支持删除、筛选、远程更新,你起拖时拿到的 dragIndex 对应的 item,在落位之前可能已经被别的操作换走了。

稳妥的做法是起拖时记录 item 的稳定 ID,而不是只记下标。落位时按 ID 找到当前真实位置,再做搬移。数组里存的是业务对象时,id 比 index 更适合做拖拽锚点。顺序持久化也是一样:存 ID 序列,不存下标序列。下标是视图的临时坐标,ID 才是数据的身份。

拖拽通道不是排序专用

UDMF 的拖拽体系设计初衷是跨应用、跨窗口搬运数据——把图片拖进聊天框、把文件拖进文件夹。排序只是把起点和终点放在同一个列表里的特例。理解这一点,你就不会去指望 onDragStart 里塞进去的对象在落位时还是同一个引用。拖拽传输的是数据快照,不是共享内存。落位之后按 ID 找到当前数组里的最新对象,把快照里的业务字段合并进去,比直接替换整个对象安全。

排序看起来只是“长按、拖动、松手”,链路里真正容易出错的永远是数据搬移的时机和下标有效性。先修落点修正,再修身份稳定,最后才谈动画和手感。顺序对了,拖拽就是顺的;下标错了,手感再好数据也是乱的。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐