react-native拖动组件开发

很多人可能都做过 html 的拖动,但是估计做 react-native 的拖动的人就不是特别多了。这里分享一下之前做的一个组件的设计思路。后面主要写了一些伪代码,主要是提供一个写这个组件的思路,具体的源码可以查看下面这个地址。

源码地址:
react-native-draggable-grid

关键 api

在讲关键算法之前先说一下需要用的几个关键方法和参数

react-native 提供了一个 PanResponder 用于识别手势操作

onPanResponderMove

onPanResponderMove: (event, gestureState) => {}

gestureState

  • x0 当响应器产生时的屏幕横坐标, 响应器一直没释放的话,这个值是一直不变的
  • y0 当响应器产生时的屏幕纵坐标,响应器一直没释放的话,这个值是一直不变的
  • moveX 最近一次移动时,手指在屏幕上相对于屏幕的x坐标,屏幕最左上角坐标为0,0
  • moveY 最近一次移动时,手指在屏幕上相对于屏幕的y坐标,屏幕最左上角坐标为0,0

开始

我们这个组件有一个需要注意的是,组件会被限制在只能在一定的区域内拖动,不能超过这个区域。

第一步

首先我们需要在组件构造函数里实例化一个 panResponder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

this.panResponderCapture = false;
this.panResponder = PanResponder.create({
//否在触摸开始时想成为响应器?
onStartShouldSetPanResponder:() => true,

// 这两个用于设置是否成为响应器,当手指在视图上移动的时候回不断的调用者两个方法
onMoveShouldSetPanResponder:() => this.panResponderCapture,
onMoveShouldSetPanResponderCapture:() => this.panResponderCapture,

// 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
// 默认返回true。目前暂时只支持android。
onShouldBlockNativeResponder:() => false,

//其他组件想成为响应器。这个视图应该释放应答吗?返回 true 就是允许释放
onPanResponderTerminationRequest:() => false,

// 手势相应开始时触发的
onPanResponderGrant:this.onStartDrag.bind(this),

// 手势移动时触发的
onPanResponderMove:this.onHandMove.bind(this),

// 手指释放时触发
onPanResponderRelease:this.onHandRelease.bind(this),
});

第二步

GestureResponderHandlers 赋给对应的组件,为了较简单的去定位元素,我们选择了用 absolute 定位。

1
2
3
4
5
6

<View
ref={'test'}
style={{width:100,height:100,position:'absolute',backgroundColor:'red'}}
{...this.panResponder.panHandlers}
/>

第三步

这一步主要是设置 currentPosition.setOffset({x,y,});, 这个的作用是,以后每次我们调用 setValue 的时候都会调用在其基础上加上x,y,后面会详细讲解为什么要这样设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private onStartDrag(nativeEvent:GestureResponderEvent, gestureState:PanResponderGestureState) {
const activeItem = this.getActiveItem();
if (!activeItem) return false;
this.props.onDragStart && this.props.onDragStart(activeItem.itemData);
const {x0, y0, moveX, moveY} = gestureState;
const activeOrigin = this.blockPositions[this.orderMap[activeItem.key].order];
const x = activeOrigin.x - x0;
const y = activeOrigin.y - y0;

activeItem.currentPosition.setOffset({
x,
y,
});
this.activeBlockOffset = {
x,
y
};
activeItem.currentPosition.setValue({
x:moveX,
y:moveY,
})
}

第四步

这一步主要是计算出手指拖动的位置,然后重新设置 view 的left和right,后面详细讲解一下整个的计算思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private onHandMove(nativeEvent:GestureResponderEvent, gestureState:PanResponderGestureState) {
const activeItem = this.getActiveItem();
if (!activeItem) return false;
const {moveX, moveY} = gestureState;

const yChokeAmount = Math.max(0, (this.activeBlockOffset.y + moveY) - (this.state.gridLayout.height - this.state.blockHeight));
const xChokeAmount = Math.max(0, (this.activeBlockOffset.x + moveX) - (this.state.gridLayout.width - this.state.blockWidth));
const yMinChokeAmount = Math.min(0, this.activeBlockOffset.y + moveY);
const xMinChokeAmount = Math.min(0, this.activeBlockOffset.x + moveX);

const dragPosition = {
x:moveX - xChokeAmount - xMinChokeAmount,
y:moveY - yChokeAmount - yMinChokeAmount,
};
activeItem.currentPosition.setValue(dragPosition);
}

我们来看一下我们从下面这个位置,移动到上面这个这个位置是如何计算出新的位置的坐标。新的y等于 y - (moveY1 - moveY2) = y -moveY1 + moveY2,我们onStartDrag的setOffset做的其实就是先把y - moveY1 这部分算出来缓存起来。后面我们只需要直接加上最新moveY即为最终结果。

现在再来看一下如何把组件限制在一定区域内移动:

最上边界

我们先看纵坐标我们能移动到的最上面的位置。y的值最小为0,即要控制offsetY + moveY >= 0

如果组件移动超过了限制区域,newY = moveY + offsetY,newY 是个负值,此时我们需要把newY向下偏移到0

最下边界

要控制vie移动的最下面区域之上,我们需要控制 offsetY + moveY <= parentHeight - blockHeight

如果区域超出了最下边的底线,超出值等于 (offsetY + moveY - (parentHeight - blockHeight)), 如果超出的话这个值为正值,我们的offsetY + moveY需要减去这个值,不超出的话则是负值

偏移算法

上面接触到最上边界和最下边界同时只会发生一个,我们可以通过修正offsetY + moveY来达到把y限制在一定区域内。

1
2
3
4
5
6
7
8
// 上偏移调整值
const yMinChokeAmount = Math.min(0, this.activeBlockOffset.y + moveY);

// 下偏移调整值
const yChokeAmount = Math.max(0, (this.activeBlockOffset.y + moveY) - (this.state.gridLayout.height - this.state.blockHeight));

// 偏移调整后的y
const y = moveY - yChokeAmount - yMinChokeAmount;

x 的偏移调整也是同理。

最后

最后就是在手指释放的时候将 panResponderCapture 设置回 false

1
2
3
4

private onHandRelease() {
this.panResponderCapture = false;
}

组件还有涉及到数据同步,定位,排序等算法,这里就不做细讲了,有兴趣的可以去看源码研究一下。-