HarmonyOS 7 拖拽排序与手势冲突重构手记
上周产品经理跑过来,指着我们的任务管理模块说:“用户提意见了,清单里的任务不能拖拽排序,每次都要手动改优先级数字,太反人类了。这周把长按拖拽排序安排上。”
我寻思着 ArkUI 的原生拖拽能力应该挺完善的,直接加个 draggable 属性不就行了?结果真机一跑,测试同学直接发飙:“我长按想拖拽,结果它直接触发了侧滑菜单!好不容易拖起来了,原来位置的卡片直接塌陷,整个列表疯狂闪动。”为了填平这个交互的深水区,我花了整整两天时间重构了列表的手势与拖拽逻辑。
被系统原生长按拦截的滑动事件
我们先来看最基础的拖拽。ArkUI 里让一个组件可拖拽,最直接的方式是设置 draggable(true),并绑定 onDragStart。
typescript
ListItem() {
TaskCard({ task: item })
}
.draggable(true) // 开启拖拽
.onDragStart((event: DragEvent) => {
if (event) {
// 返回拖拽时携带的数据
return { extraParams: JSON.stringify(item) };
}
return null;
})
写完一跑,灾难出现了:为了做侧滑删除,我原本在这个 ListItem 上套了一个 SwipeAction 或者自定义了 LongPressGesture。长按事件和拖拽事件在底层的手势识别器里打架了。系统根本分不清你是想长按弹菜单,还是想长按拖拽,导致画面疯狂闪烁,甚至应用直接卡死。
警示:在长列表里做拖拽,千万不要在业务层自己写 LongPressGesture 去触发状态。只要设置了 draggable(true),系统底层会自带一个长按识别的时序。如果你外层有冲突的手势,必须用 .gesture() 配合优先级去拦截,或者干脆把侧滑菜单的优先级调低。
填补拖拽过程中的布局空洞
解决了能拖起来的问题,下一个痛点就是视觉体验。当卡片被你拎起来的时候,它原本的坑位直接被后面的元素顶上去了,整个列表瞬间发生重排。
想要那种“原位置留个透明框,其他元素平滑避让”的高级感,必须在拖拽开始时,给原组件做一个透明化处理,并利用 customPreview 自定义拖拽时跟随手指的浮层。
typescript
@Builder
function DragPreviewBuilder(item: TaskItem) {
TaskCard({ task: item })
.width('90%')
.shadow({ radius: 15, color: '#33000000' }) // 加上阴影,提升悬浮感
.scale({ x: 1.05, y: 1.05 }) // 稍微放大
}
// 在 ListItem 里应用
ListItem() {
TaskCard({ task: item })
}
.draggable(true)
.onDragStart((event: DragEvent) => {
// 核心动作:返回预览图和数据
return {
builder: DragPreviewBuilder.bind(this, item),
extraParams: JSON.stringify(item)
} as DragItemInfo;
})
// 关键:拖拽开始时,把原本的组件隐藏,保留占位
.opacity(this.draggingItemId === item.id ? 0 : 1)
配合 onDragStart 里把被拖拽的组件 .opacity(0),这样底层框架就会保留它原本的布局空间,其他卡片就不会瞎跳了。
处理落位释放与数据重排的平滑动画
当用户手指松开,组件掉落在目标位置时。我们需要在父容器的 onDrop 里接管逻辑。
这里有个极其隐蔽的深坑:onDrop 回调里拿到的数据是字符串。你必须在用 JSON.parse 之前做好 try-catch,因为有时候系统传过来的可能是空字符串,或者是别的格式,直接解析会让应用闪退。
typescript
List() {
// ForEach 渲染列表
}
.onDrop((event: DragEvent) => {
if (event && event.getData()) {
try {
const droppedData = JSON.parse(event.getData().extraParams);
const targetIndex = this.getIndexByPosition(event.getData().touchPoint); // 计算落点索引
// 核心动作:使用 animateTo 包裹数据操作,实现平滑重排
animateTo({ duration: 300, curve: Curve.Friction }, () => {
// 从原数组删除,插入到新位置
const sourceIndex = this.tasks.findIndex(t => t.id === droppedData.id);
this.tasks.splice(sourceIndex, 1);
this.tasks.splice(targetIndex, 0, droppedData);
});
} catch (err) {
console.error('拖拽数据解析失败', err);
}
}
})
计算落点索引也是个技术活。不能简单地用 Y 坐标去除以组件高度,因为列表可能有滚动偏移。必须结合 List 的 currentOffset 或者用 onItemDragMove 自带的 insertIndex 属性来动态计算。
多端适配下的长按冲突防御
拖拽功能在折叠屏或者平板上,如果同时连着鼠标,还会引发鼠标点击事件和拖拽事件的叠加。用户用鼠标长按拖动时,可能会触发两次落位动作。
必须在应用层加一把状态锁,或者判断当前设备的输入源。综合来看,拖拽开发看着只是几个 API 的调用,底层却涉及到了手势派发、布局重绘和状态管理的多重博弈。把占位填补好,把动画跟手度调稳,你的应用交互才算真正摸到了高级的边。
更多推荐

所有评论(0)