React native(0.71) 当前还没有官方的Drag and Drop的支持。最近的项目中需要在客户端上用类似Drag and Drop这样的操作来改善一个功能的交互效果。这里记录一下是如何通过 Reanimated 和 gesture-handler 实现这个功能的。
实现Drag and Drop的交互主要需要解决以下几个问题:
如何在Drag时实现View跟随touch的轨迹移动?
接受Drop的View如何感知Dragging的轨迹到达了该View的区域内?
如何在Drop操作时将Drag和Drop的对象传递给 parent
下面将分别说明这几个问题是如何解决的
Drag的效果
在react-native-gesture-handler
里,我们可以通过Pan gesture
实现Drag的效果:
1 | const positionX = useSharedValue(0); |
我们通过两个SharedValue
来记录touch的轨迹,然后通过AnimatedStyle对控件的位置进行修改,就达到了Drag时View跟随的效果:
1 | const dragStyle = useAnimatedStyle(() => { |
Drop Zone的进入感知
对于可以Drop的区域,当Dragging进入该控件时,提供一个highlight的效果让用户知道操作可以进行是一个比较好的用户交互。Drop Zone通常和Draggable的View并不存在包含隶属的关系,所以我们需要让它们有一个共同的parent的控件来传递dragging时的touch所在的位置信息。
1 | const draggingPos = useSharedValue({ x: -1, y: -1 }); |
然后在draggingGesture
的onUpdate中更新位置信息:
1 | runOnJS(tracking)(e.absoluteX, e.absoluteY); |
需要注意的是,这里需要用runOnJS来调用tracking。
有了位置信息,我们还需要知道Drop Zone的绝对位置信息,可以通过view的onLayout的调用时获取
1 | const onLayout = () => { |
这样我们就可以在Drop Zone中检测dragging的touch是否进入到Drop Zone的内部,并通过animatedStyle实时改变自己的状态用于通知用户
1 | //Detect the dragging pointer is inside this zone or not |
Drop的事件处理
最后,我们需要处理Drop的事件。Drop的事件是由draggingGesture来触发的,在其onEnd的方法中,我们可以检查此时touch是否处于drop zone中,如果在drop zone中则触发drop事件。
在onLayout新增一个registerView, 将当前Drop zone的位置和区域大小的信息传递给父类
1
registerView(id, x, y, width, height);
修改后的draggingGesture如下,在onEnd中新增dragAndDrop的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const draggingGesture = Gesture.Pan()
.maxPointers(1)
.activateAfterLongPress(100)
.onStart(() => {
active.value = true;
})
.onUpdate((e) => {
positionX.value = e.translationX;
positionY.value = e.translationY;
runOnJS(tracking)(e.absoluteX, e.absoluteY);
})
.onEnd((e) => {
if (!runOnJS(dragAndDrop)(e.absoluteX, e.absoluteY, id)) {
positionX.value = withTiming(0);
positionY.value = withTiming(0);
}
active.value = false;
runOnJS(tracking)(-1, -1)
});由父类检测是否处于drop zone,并调用onDrop
1
2
3
4
5
6
7
8
9
10
11const dragAndDrop = (x, y, id) => {
const view = viewRegister.value.find(({rect}) => {
return isInside(x, y, rect)
})
if (view) {
onDrop(id, view.id)
return true
}
return false;
};