react-native-drag-sort
Version:
Drag and drop sort control for react-native
527 lines (454 loc) • 18.8 kB
JavaScript
import React, {Component} from 'react'
import {Animated, Dimensions, Easing, PanResponder, StyleSheet, TouchableOpacity, View} from 'react-native'
const PropTypes = require('prop-types')
const {width} = Dimensions.get('window')
const defaultZIndex = 8
const touchZIndex = 99
export default class DragSortableView extends Component{
constructor(props) {
super(props)
this.sortRefs = new Map()
const itemWidth = props.childrenWidth+props.marginChildrenLeft+props.marginChildrenRight
const itemHeight = props.childrenHeight+props.marginChildrenTop+props.marginChildrenBottom
// this.reComplexDataSource(true,props) // react < 16.3
// react > 16.3 Fiber
const rowNum = parseInt(props.parentWidth/itemWidth);
const dataSource = props.dataSource.map((item,index)=>{
const newData = {}
const left = (index%rowNum)*itemWidth
const top = parseInt((index/rowNum))*itemHeight
newData.data = item
newData.originIndex = index
newData.originLeft = left
newData.originTop = top
newData.position = new Animated.ValueXY({
x: parseInt(left+0.5),
y: parseInt(top+0.5),
})
newData.scaleValue = new Animated.Value(1)
return newData
});
this.state = {
dataSource: dataSource,
curPropsDataSource: props.dataSource,
height: Math.ceil(dataSource.length / rowNum) * itemHeight,
itemWidth,
itemHeight,
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
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,
})
}
// react < 16.3
// componentWillReceiveProps(nextProps) {
// if (this.props.dataSource != nextProps.dataSource) {
// this.reComplexDataSource(false,nextProps)
// }
// }
// react > 16.3 Fiber
static getDerivedStateFromProps(nextprops, prevState) {
const itemWidth = nextprops.childrenWidth + nextprops.marginChildrenLeft + nextprops.marginChildrenRight
const itemHeight = nextprops.childrenHeight + nextprops.marginChildrenTop + nextprops.marginChildrenBottom
if (nextprops.dataSource != prevState.curPropsDataSource || itemWidth !== prevState.itemWidth || itemHeight !== prevState.itemHeight) {
const rowNum = parseInt(nextprops.parentWidth / itemWidth);
const dataSource = nextprops.dataSource.map((item, index) => {
const newData = {};
const left = index % rowNum * itemWidth;
const top = parseInt(index / rowNum) * itemHeight;
newData.data = item;
newData.originIndex = index;
newData.originLeft = left;
newData.originTop = top;
newData.position = new Animated.ValueXY({
x: parseInt(left + 0.5),
y: parseInt(top + 0.5),
});
newData.scaleValue = new Animated.Value(1);
return newData;
});
return {
dataSource: dataSource,
curPropsDataSource: nextprops.dataSource,
height: Math.ceil(dataSource.length / rowNum) * itemHeight,
itemWidth,
itemHeight,
}
}
return null;
}
startTouch(touchIndex) {
//防止拖动
const fixedItems = this.props.fixedItems;
if (fixedItems.length > 0 && fixedItems.includes(touchIndex)){
return;
}
this.isHasMove = false
if (!this.props.sortable) return
const key = this._getKey(touchIndex);
if (this.sortRefs.has(key)) {
if (this.props.onDragStart) {
this.props.onDragStart(touchIndex)
}
Animated.timing(
this.state.dataSource[touchIndex].scaleValue,
{
toValue: this.props.maxScale,
duration: this.props.scaleDuration,
useNativeDriver: false,
}
).start(()=>{
this.touchCurItem = {
ref: this.sortRefs.get(key),
index: touchIndex,
originLeft: this.state.dataSource[touchIndex].originLeft,
originTop: this.state.dataSource[touchIndex].originTop,
moveToIndex: touchIndex,
}
this.isMovePanResponder = true
})
}
}
moveTouch (nativeEvent,gestureState) {
this.isHasMove = true
//if (this.isScaleRecovery) clearTimeout(this.isScaleRecovery)
if (this.touchCurItem) {
let dx = gestureState.dx
let dy = gestureState.dy
const itemWidth = this.state.itemWidth;
const itemHeight = this.state.itemHeight;
const rowNum = parseInt(this.props.parentWidth/itemWidth);
const maxWidth = this.props.parentWidth-itemWidth
const maxHeight = itemHeight*Math.ceil(this.state.dataSource.length/rowNum) - itemHeight
// Is it free to drag
if (!this.props.isDragFreely) {
// Maximum or minimum after out of bounds
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,
}
})
this.state.dataSource[this.touchCurItem.index].position.setValue({
x: left,
y: top,
})
let moveToIndex = 0
let moveXNum = dx/itemWidth
let moveYNum = dy/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
} else if (moveToIndex < 0) {
moveToIndex = 0;
}
if (this.props.onDragging) {
this.props.onDragging(gestureState, left, top, moveToIndex)
}
if (this.touchCurItem.moveToIndex != moveToIndex ) {
const fixedItems = this.props.fixedItems;
if (fixedItems.length > 0 && fixedItems.includes(moveToIndex)) return;
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: this.props.slideDuration,
easing: Easing.out(Easing.quad),
useNativeDriver: false,
}
).start()
}
})
}
}
}
endTouch (nativeEvent) {
//clear
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: this.props.scaleDuration,
useNativeDriver: false,
}
).start(()=>{
this.touchCurItem.ref.setNativeProps({
style: {
zIndex: defaultZIndex,
}
})
this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex)
this.touchCurItem = null
})
}
}
onPressOut () {
this.isScaleRecovery = setTimeout(()=> {
if (this.isMovePanResponder && !this.isHasMove) {
this.endTouch()
}
},220)
}
changePosition(startIndex,endIndex) {
if (startIndex == endIndex) {
const curItem = this.state.dataSource[startIndex]
if (curItem != null) {
curItem.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())
}
// Prevent RN from drawing the beginning and end
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),
})
})
}
reComplexDataSource(isInit,props) {
const itemWidth = this.state.itemWidth;
const itemHeight = this.state.itemHeight;
const rowNum = parseInt(props.parentWidth/itemWidth);
const dataSource = props.dataSource.map((item,index)=>{
const newData = {}
const left = (index%rowNum)*itemWidth
const top = parseInt((index/rowNum))*itemHeight
newData.data = item
newData.originIndex = index
newData.originLeft = left
newData.originTop = top
newData.position = new Animated.ValueXY({
x: parseInt(left+0.5),
y: parseInt(top+0.5),
})
newData.scaleValue = new Animated.Value(1)
return newData
})
if (isInit) {
this.state = {
dataSource: dataSource,
height: Math.ceil(dataSource.length/rowNum)*itemHeight
}
} else {
this.setState({
dataSource: dataSource,
height: Math.ceil(dataSource.length/rowNum)*itemHeight
})
}
}
getOriginalData () {
return this.state.dataSource.map((item,index)=> item.data)
}
render() {
return (
<View
//ref={(ref)=>this.sortParentRef=ref}
style={[styles.container,{
width: this.props.parentWidth,
height: this.state.height,
}]}
//onLayout={()=> {}}
>
{this._renderItemView()}
</View>
)
}
_getKey = (index) => {
const item = this.state.dataSource[index];
return this.props.keyExtractor ? this.props.keyExtractor(item.data, index) : item.originIndex;
}
_renderItemView = () => {
const {maxScale, minOpacity} = this.props
const inputRange = maxScale >= 1 ? [1, maxScale] : [maxScale, 1]
const outputRange = maxScale >= 1 ? [1, minOpacity] : [minOpacity, 1]
return this.state.dataSource.map((item,index)=>{
const transformObj = {}
transformObj[this.props.scaleStatus] = item.scaleValue
const key = this._getKey(index);
return (
<Animated.View
key={key}
ref={(ref) => this.sortRefs.set(key,ref)}
{...this._panResponder.panHandlers}
style={[styles.item,{
marginTop: this.props.marginChildrenTop,
marginBottom: this.props.marginChildrenBottom,
marginLeft: this.props.marginChildrenLeft,
marginRight: this.props.marginChildrenRight,
left: item.position.x,
top: item.position.y,
opacity: item.scaleValue.interpolate({inputRange,outputRange}),
transform: [transformObj]
}]}>
<TouchableOpacity
activeOpacity = {1}
delayLongPress={this.props.delayLongPress}
onPressOut={()=> this.onPressOut()}
onLongPress={()=>this.startTouch(index)}
onPress={()=>{
if (this.props.onClickItem) {
this.props.onClickItem(this.getOriginalData(),item.data,index)
}
}}>
{this.props.renderItem(item.data,index)}
</TouchableOpacity>
</Animated.View>
)
})
}
componentWillUnmount() {
if (this.isScaleRecovery) clearTimeout(this.isScaleRecovery)
}
}
DragSortableView.propTypes = {
dataSource: PropTypes.array.isRequired,
parentWidth: PropTypes.number,
childrenHeight: PropTypes.number.isRequired,
childrenWidth: PropTypes.number.isRequired,
marginChildrenTop: PropTypes.number,
marginChildrenBottom: PropTypes.number,
marginChildrenLeft: PropTypes.number,
marginChildrenRight: PropTypes.number,
sortable: PropTypes.bool,
onClickItem: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
onDataChange: PropTypes.func,
renderItem: PropTypes.func.isRequired,
scaleStatus: PropTypes.oneOf(['scale','scaleX','scaleY']),
fixedItems: PropTypes.array,
keyExtractor: PropTypes.func,
delayLongPress: PropTypes.number,
isDragFreely: PropTypes.bool,
onDragging: PropTypes.func,
maxScale: PropTypes.number,
minOpacity: PropTypes.number,
scaleDuration: PropTypes.number,
slideDuration: PropTypes.number
}
DragSortableView.defaultProps = {
marginChildrenTop: 0,
marginChildrenBottom: 0,
marginChildrenLeft: 0,
marginChildrenRight: 0,
parentWidth: width,
sortable: true,
scaleStatus: 'scale',
fixedItems: [],
isDragFreely: false,
maxScale: 1.1,
minOpacity: 0.8,
scaleDuration: 100,
slideDuration: 300,
}
const styles = StyleSheet.create({
container: {
flexWrap: 'wrap',
flexDirection: 'row',
},
item: {
position: 'absolute',
zIndex: defaultZIndex,
},
})