0%

React native 下的Drag and Drop

React native(0.71) 当前还没有官方的Drag and Drop的支持。最近的项目中需要在客户端上用类似Drag and Drop这样的操作来改善一个功能的交互效果。这里记录一下是如何通过 Reanimated 和 gesture-handler 实现这个功能的。

实现Drag and Drop的交互主要需要解决以下几个问题:

  1. 如何在Drag时实现View跟随touch的轨迹移动?

  2. 接受Drop的View如何感知Dragging的轨迹到达了该View的区域内?

  3. 如何在Drop操作时将Drag和Drop的对象传递给 parent

    下面将分别说明这几个问题是如何解决的

Drag的效果

react-native-gesture-handler里,我们可以通过Pan gesture实现Drag的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const positionX = useSharedValue(0);
const positionY = useSharedValue(0);

const draggingGesture = Gesture.Pan()
.maxPointers(1)
.activateAfterLongPress(100)
.onStart(() => {
active.value = true;
})
.onUpdate((e) => {
positionX.value = e.translationX;
positionY.value = e.translationY;

})
.onEnd((e) => {

positionX.value = withTiming(0);
positionY.value = withTiming(0);
}
active.value = false;

});


我们通过两个SharedValue来记录touch的轨迹,然后通过AnimatedStyle对控件的位置进行修改,就达到了Drag时View跟随的效果:

1
2
3
4
5
6
7
8
9
10
const dragStyle = useAnimatedStyle(() => {
return {
opacity: active.value ? 0.5 : 1,
borderColor: active.value ? 'red' : 'transparent',
transform: [
{ translateX: positionX.value },
{ translateY: positionY.value },
],
};
});

Drop Zone的进入感知

对于可以Drop的区域,当Dragging进入该控件时,提供一个highlight的效果让用户知道操作可以进行是一个比较好的用户交互。Drop Zone通常和Draggable的View并不存在包含隶属的关系,所以我们需要让它们有一个共同的parent的控件来传递dragging时的touch所在的位置信息。

1
2
3
4
5
6
const draggingPos = useSharedValue({ x: -1, y: -1 });

const tracking = (x, y) => {
//console.log(x, y)
draggingPos.value = { x, y };
};

然后在draggingGesture的onUpdate中更新位置信息:

1
runOnJS(tracking)(e.absoluteX, e.absoluteY);

需要注意的是,这里需要用runOnJS来调用tracking。

有了位置信息,我们还需要知道Drop Zone的绝对位置信息,可以通过view的onLayout的调用时获取

1
2
3
4
5
6
const onLayout = () => {
viewRef.current?.measureInWindow((x, y, width, height) => {
viewRect.value = { x, y, width, height };

});
};

这样我们就可以在Drop Zone中检测dragging的touch是否进入到Drop Zone的内部,并通过animatedStyle实时改变自己的状态用于通知用户

1
2
3
4
5
6
7
8
9
10
11
12
13
//Detect the dragging pointer is inside this zone or not
const draggingInView = useDerivedValue(() => {
return isInside(draggingPos.value.x, draggingPos.value.y, viewRect.value);
}, [draggingPos, viewRect]);

const zoneStyle = useAnimatedStyle(() => {
return {
borderColor: draggingInView.value ? "red" : "gray",
borderWidth: 2,
borderRadius: 5,
borderStyle: "dashed"
}
})

Drop的事件处理

最后,我们需要处理Drop的事件。Drop的事件是由draggingGesture来触发的,在其onEnd的方法中,我们可以检查此时touch是否处于drop zone中,如果在drop zone中则触发drop事件。

  1. 在onLayout新增一个registerView, 将当前Drop zone的位置和区域大小的信息传递给父类

    1
    registerView(id, x, y, width, height);
  2. 修改后的draggingGesture如下,在onEnd中新增dragAndDrop的调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const 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)
    });
  3. 由父类检测是否处于drop zone,并调用onDrop

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const 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;
    };

示例