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



  • 最近由于业务需求需要实现一个功能需要实现图片的上传和排序和删除,在网上搜索了几款发现都需要固定列数,感觉不太友好,所以自己实现了一个可以不需要设定列数的排序,而且布局高度实现自适应

    源码链接

    效果图对比(固定列数和自适应流布局)

    [图片上传中...(iphone.jpg-9f7224-1533711885416-0)]

    iphone.jpg

    动态图

    Demonstration.gif

    实现

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

    梳理一下步骤
      1. 开始触发: 长按或触摸到达一定时间时触发开始排序,这时可以进行把被单机的item放大、透明、抖动动画。
      1. 开始滑动:
      • (1) 被拖拽的item随着手指的滑动而滑动
      • (2) 被拖动的item滑动到第x个时,item到x之间的item进行左滑右滑一个位置的动画。
      1. 松开手指:
      • (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渐变动画


    源码链接



  • 兄弟,你这么努力,但是安卓上方性能还是有点差啊,屏幕旋转支持也不好。 为何不考虑考虑fork我的这个 去改呢?

    https://github.com/bolan9999/react-native-drag-to-sort-tags

    只不过没去做抖动动画而已,抖动动画又不是难点