一步步实现一个自适应的react-native拖拽排序

5次阅读

共计 6245 个字符,预计需要花费 16 分钟才能阅读完成。

2019.2: 优化拖拽不移动时自动恢复,现在这个插件应该没有任何问题。新加一个实战演示例子,后面有时间会对这个例子进行加动画,删除时 item 向下到待选的 item 动画,和待选到 item。还有滑动时自动向下滑动动画。

最近由于业务需求需要实现一个功能需要实现图片的上传和排序和删除,在网上搜索了几款发现都需要固定列数,感觉不太友好,所以自己实现了一个可以不需要设定列数的排序,而且布局高度实现自适应。
源码链接
效果图对比(固定列数和自适应流布局)
![[图片上传中 …(iphone.jpg-9f7224-1533711885416-0)]](https://user-gold-cdn.xitu.io…

动态图

实现
其实拖拽排序在大多数编程语言里已经有很多中三方插件可以使用, 实现方法都差不多,而且例如 Android 和 iOS 或者现在的 React-Native 他们逻辑几乎是可以共用,你会写一个语言的拖拽排序,其他的都差不多。
梳理一下步骤

开始触发:长按或触摸到达一定时间时触发开始排序,这时可以进行把被单机的 item 放大、透明、抖动动画。

开始滑动:

(1) 被拖拽的 item 随着手指的滑动而滑动
(2) 被拖动的 item 滑动到第 x 个时,item 到 x 之间的 item 进行左滑右滑一个位置的动画。

松开手指:

(1) 被拖拽的这个 item 通过四舍五入进入相应的位置。
(2) 数据进行替换并重绘加布局矫正。

tip: 滑动逻辑,例如当你把 index= 1 拖到 index=3,不是将 1 和 3 替换 (0,3,2,1,4),而是(0,3,1,2,4) 这才是拖拽后结果,只将被拖拽的一个替换到要去的位置,其他的向前和向后移动
主要代码
// 触摸事件的监听
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => this.props.sortable,
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
this.isMovePanResponder = false
return false
},
// 接管触摸加滑动事件
onMoveShouldSetPanResponder: (evt, gestureState) => this.isMovePanResponder,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.isMovePanResponder,

onPanResponderGrant: (evt, gestureState) => {},
onPanResponderMove: (evt, gestureState) => this.moveTouch(evt,gestureState),
onPanResponderRelease: (evt, gestureState) => this.endTouch(evt),

onPanResponderTerminationRequest: (evt, gestureState) => false,
onShouldBlockNativeResponder: (evt, gestureState) => false,
})

// 这里使用长按触发开发拖拽事件,其实开始是使用触摸一定时间后触发事件的,但和 View 的单机事件有冲突不好解决,所以选择了长按触发事件
startTouch(touchIndex) {
// 接管滑动
this.isMovePanResponder = true
//if (this.measureTimeOut) clearTimeout(this.measureTimeOut)

if (sortRefs.has(touchIndex)) {
if (this.props.onDragStart) {
this.props.onDragStart(touchIndex)
}
// 变大和加透明
Animated.timing(
this.state.dataSource[touchIndex].scaleValue,
{
toValue: maxScale,
duration: scaleDuration,
}
).start(()=>{
// 备份被触摸的事件
this.touchCurItem = {
ref: sortRefs.get(touchIndex),
index: touchIndex,
// 记录之前的位置
originLeft: this.state.dataSource[touchIndex].originLeft,
originTop: this.state.dataSource[touchIndex].originTop,
moveToIndex: touchIndex,
}
})
}
}

// 滑动
moveTouch (nativeEvent,gestureState) {
if (this.touchCurItem) {

let dx = gestureState.dx
let dy = gestureState.dy

const rowNum = parseInt(this.props.parentWidth/this.itemWidth);
const maxWidth = this.props.parentWidth-this.itemWidth
const maxHeight = this.itemHeight*Math.ceil(this.state.dataSource.length/rowNum) – this.itemHeight

// 出界后取最大或最小值防止出界
if (this.touchCurItem.originLeft + dx < 0) {
dx = -this.touchCurItem.originLeft
} else if (this.touchCurItem.originLeft + dx > maxWidth) {
dx = maxWidth – this.touchCurItem.originLeft
}
if (this.touchCurItem.originTop + dy < 0) {
dy = -this.touchCurItem.originTop
} else if (this.touchCurItem.originTop + dy > maxHeight) {
dy = maxHeight – this.touchCurItem.originTop
}

let left = this.touchCurItem.originLeft + dx
let top = this.touchCurItem.originTop + dy
// 置于最上层
this.touchCurItem.ref.setNativeProps({
style: {
zIndex: touchZIndex,
}
})

// 滑动时刷新布局,这里直接刷新 Animated 的数字就可以进行局部刷新了
this.state.dataSource[this.touchCurItem.index].position.setValue({
x: left,
y: top,
})

let moveToIndex = 0
let moveXNum = dx/this.itemWidth
let moveYNum = dy/this.itemHeight
if (moveXNum > 0) {
moveXNum = parseInt(moveXNum+0.5)
} else if (moveXNum < 0) {
moveXNum = parseInt(moveXNum-0.5)
}
if (moveYNum > 0) {
moveYNum = parseInt(moveYNum+0.5)
} else if (moveYNum < 0) {
moveYNum = parseInt(moveYNum-0.5)
}

moveToIndex = this.touchCurItem.index+moveXNum+moveYNum*rowNum

if (moveToIndex > this.state.dataSource.length-1) moveToIndex = this.state.dataSource.length-1

// 其他 item 向左和向右滑动
if (this.touchCurItem.moveToIndex != moveToIndex) {
this.touchCurItem.moveToIndex = moveToIndex

this.state.dataSource.forEach((item,index)=>{

let nextItem = null
if (index > this.touchCurItem.index && index <= moveToIndex) {
nextItem = this.state.dataSource[index-1]

} else if (index >= moveToIndex && index < this.touchCurItem.index) {
nextItem = this.state.dataSource[index+1]

} else if (index != this.touchCurItem.index &&
(item.position.x._value != item.originLeft ||
item.position.y._value != item.originTop)) {
nextItem = this.state.dataSource[index]

// 有时前一个或者后一个数据有个动画差的原因无法回到正确位置,这里进行矫正
} else if ((this.touchCurItem.index-moveToIndex > 0 && moveToIndex == index+1) ||
(this.touchCurItem.index-moveToIndex < 0 && moveToIndex == index-1)) {
nextItem = this.state.dataSource[index]
}

// 需要滑动的就进行滑动动画
if (nextItem != null) {
Animated.timing(
item.position,
{
toValue: {x: parseInt(nextItem.originLeft+0.5),y: parseInt(nextItem.originTop+0.5)},
duration: slideDuration,
easing: Easing.out(Easing.quad),
}
).start()
}

})
}

}
}

// 触摸事件
endTouch (nativeEvent) {

//clear
if (this.measureTimeOut) clearTimeout(this.measureTimeOut)

if (this.touchCurItem) {
if (this.props.onDragEnd) {
this.props.onDragEnd(this.touchCurItem.index,this.touchCurItem.moveToIndex)
}
//this.state.dataSource[this.touchCurItem.index].scaleValue.setValue(1)
Animated.timing(
this.state.dataSource[this.touchCurItem.index].scaleValue,
{
toValue: 1,
duration: scaleDuration,
}
).start()
this.touchCurItem.ref.setNativeProps({
style: {
zIndex: defaultZIndex,
}
})
this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex)
this.touchCurItem = null
}
}

// 刷新数据
changePosition(startIndex,endIndex) {

if (startIndex == endIndex) {
const curItem = this.state.dataSource[startIndex]
this.state.dataSource[startIndex].position.setValue({
x: parseInt(curItem.originLeft+0.5),
y: parseInt(curItem.originTop+0.5),
})
return;
}

let isCommon = true
if (startIndex > endIndex) {
isCommon = false
let tempIndex = startIndex
startIndex = endIndex
endIndex = tempIndex
}

const newDataSource = […this.state.dataSource].map((item,index)=>{
let newIndex = null
if (isCommon) {
if (endIndex > index && index >= startIndex) {
newIndex = index+1
} else if (endIndex == index) {
newIndex = startIndex
}
} else {
if (endIndex >= index && index > startIndex) {
newIndex = index-1
} else if (startIndex == index) {
newIndex = endIndex
}
}

if (newIndex != null) {
const newItem = {…this.state.dataSource[newIndex]}
newItem.originLeft = item.originLeft
newItem.originTop = item.originTop
newItem.position = new Animated.ValueXY({
x: parseInt(item.originLeft+0.5),
y: parseInt(item.originTop+0.5),
})
item = newItem
}

return item
})

this.setState({
dataSource: newDataSource
},()=>{
if (this.props.onDataChange) {
this.props.onDataChange(this.getOriginalData())
}
// 防止 RN 不绘制开头和结尾
const startItem = this.state.dataSource[startIndex]
this.state.dataSource[startIndex].position.setValue({
x: parseInt(startItem.originLeft+0.5),
y: parseInt(startItem.originTop+0.5),
})
const endItem = this.state.dataSource[endIndex]
this.state.dataSource[endIndex].position.setValue({
x: parseInt(endItem.originLeft+0.5),
y: parseInt(endItem.originTop+0.5),
})
})

}

后续会加上添加和删除 Item 渐变动画

源码链接

正文完
 0