一步步實現(xiàn)一個自適應(yīng)的react-native拖拽排序

2019.6 新增頂部固定功能,可以設(shè)置開始連續(xù)幾個為不可拖動功能惊橱,類似今日頭條一樣,該功能和今日頭條拖拽一樣箭昵,可以去對比一下李皇。

ezgif.com-resize.gif

2019.3: 新增單行拖拽演示,其實這個功能一致宙枷,這個拖拽插件本來就是自適應(yīng)行,有時間會整體優(yōu)化下ScrollView問題掉房,使控件自帶ScrollView功能。

one-line.gif

2019.2: 優(yōu)化拖拽不移動時自動恢復(fù)慰丛,現(xiàn)在這個插件應(yīng)該沒有任何問題卓囚。新加一個實戰(zhàn)演示例子,后面有時間會對這個例子進行加動畫诅病,刪除時item向下到待選的item動畫哪亿,和待選到item。還有滑動時自動向下滑動動畫贤笆。

demo.gif

最近由于業(yè)務(wù)需求需要實現(xiàn)一個功能需要實現(xiàn)圖片的上傳和排序和刪除蝇棉,在網(wǎng)上搜索了幾款發(fā)現(xiàn)都需要固定列數(shù),感覺不太友好芥永,所以自己實現(xiàn)了一個可以不需要設(shè)定列數(shù)的排序篡殷,而且布局高度實現(xiàn)自適應(yīng)

源碼鏈接

效果圖對比(固定列數(shù)和自適應(yīng)流布局)

[圖片上傳中...(iphone.jpg-9f7224-1533711885416-0)]
iphone.jpg

動態(tài)圖

olddemo.gif

實現(xiàn)

其實拖拽排序在大多數(shù)編程語言里已經(jīng)有很多中三方插件可以使用,實現(xiàn)方法都差不多埋涧,而且例如Android和iOS或者現(xiàn)在的React-Native他們邏輯幾乎是可以共用板辽,你會寫一個語言的拖拽排序,其他的都差不多棘催。

梳理一下步驟
    1. 開始觸發(fā): 長按或觸摸到達(dá)一定時間時觸發(fā)開始排序醇坝,這時可以進行把被單機的item放大、透明、抖動動畫。
    1. 開始滑動:
    • (1) 被拖拽的item隨著手指的滑動而滑動
    • (2) 被拖動的item滑動到第x個時,item到x之間的item進行左滑右滑一個位置的動畫嘁傀。
    1. 松開手指:
    • (1) 被拖拽的這個item通過四舍五入進入相應(yīng)的位置细办。
    • (2) 數(shù)據(jù)進行替換并重繪加布局矯正笑撞。

tip: 滑動邏輯茴肥,例如當(dāng)你把index=1拖到index=3,不是將1和3替換(0,3,2,1,4)础锐,而是(0,3,1,2,4)這才是拖拽后結(jié)果截粗,只將被拖拽的一個替換到要去的位置财破,其他的向前和向后移動

主要代碼
// 觸摸事件的監(jiān)聽
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,
        })

//這里使用長按觸發(fā)開發(fā)拖拽事件俊性,其實開始是使用觸摸一定時間后觸發(fā)事件的趟薄,但和View的單機事件有沖突不好解決杭煎,所以選擇了長按觸發(fā)事件
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的數(shù)字就可以進行局部刷新了
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]

                        //有時前一個或者后一個數(shù)據(jù)有個動畫差的原因無法回到正確位置,這里進行矯正
                    } 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
        }
    }

//刷新數(shù)據(jù)
    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不繪制開頭和結(jié)尾
            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),
            })
        })

    }

后續(xù)會加上添加和刪除Item漸變動畫


源碼鏈接


React-Native 篇

七分設(shè)計感的純React-Native項目Mung

一個完整小巧的Redux全家桶項目

react-native拖拽排序

多功能React-Native-Toast組件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滨嘱,一起剝皮案震驚了整個濱河市浸间,隨后出現(xiàn)的幾起案子囊扳,更是在濱河造成了極大的恐慌狭瞎,老刑警劉巖碗殷,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸟廓,死亡現(xiàn)場離奇詭異,居然都是意外死亡襟己,警方通過查閱死者的電腦和手機引谜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來擎浴,“玉大人员咽,你說我怎么就攤上這事≈ぃ” “怎么了贝室?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仿吞。 經(jīng)常有香客問我滑频,道長,這世上最難降的妖魔是什么唤冈? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任峡迷,我火速辦了婚禮,結(jié)果婚禮上你虹,老公的妹妹穿的比我還像新娘绘搞。我一直安慰自己,他們只是感情好傅物,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布夯辖。 她就那樣靜靜地躺著,像睡著了一般董饰。 火紅的嫁衣襯著肌膚如雪楼雹。 梳的紋絲不亂的頭發(fā)上模孩,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音贮缅,去河邊找鬼榨咐。 笑死,一個胖子當(dāng)著我的面吹牛谴供,可吹牛的內(nèi)容都是我干的块茁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼桂肌,長吁一口氣:“原來是場噩夢啊……” “哼数焊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起崎场,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤佩耳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谭跨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體干厚,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年螃宙,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛮瞄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡谆扎,死狀恐怖挂捅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情堂湖,我是刑警寧澤闲先,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站无蜂,受9級特大地震影響饵蒂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酱讶,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一退盯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泻肯,春花似錦渊迁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至稚铣,卻和暖如春箱叁,著一層夾襖步出監(jiān)牢的瞬間墅垮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工耕漱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留算色,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓螟够,卻偏偏與公主長得像灾梦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妓笙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 早上起不來若河,晚上不想回家,通勤的路上反復(fù)刪了下載寞宫,下載之后又刪除萧福,我需要轉(zhuǎn)移注意力了。 計劃元旦回來...
    撈撈醬愛吃魚閱讀 216評論 0 1
  • 膽怯的勇士今日日程安排: 001【頭等要務(wù)】 6:00的鬧鐘響了饲窿。唉煌寇,今天是難得的周末焕蹄,再睡10分鐘吧,就10分鐘...
    膽怯的勇士閱讀 189評論 0 2
  • 當(dāng)家鄉(xiāng)有一天真的成了遠(yuǎn)方阀溶,那思念的滋味是那樣酸楚腻脏。你會眺望遠(yuǎn)處的那片云,那座山银锻,想那下面應(yīng)該是家鄉(xiāng)永品。你會融...
    滿江楓飛閱讀 462評論 2 3
  • 來源:熊貓設(shè)計院(公眾號) 作者:熊貓小生 前言: 交互設(shè)計之父阿蘭·庫珀說過這樣一句話:“除非有更好的選擇,否則...
    紅小巫閱讀 2,060評論 0 3