为了账号安全,请及时绑定邮箱和手机立即绑定

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

2019.3: 新增单行拖拽演示,其实这个功能一致,这个拖拽插件本来就是自适应行,有时间会整体优化下ScrollView问题,使控件自带ScrollView功能。

one-line.gif

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

ezgif.com-video-to-gif.gif


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

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

[图片上传中...(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渐变动画



React-Native 篇

点击查看更多内容
3人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
16
获赞与收藏
105

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消