長(zhǎng)按cell拖動(dòng)排序的實(shí)現(xiàn)

前言

前不久有跟小伙伴談?wù)摰疥P(guān)于長(zhǎng)按cell來(lái)拖動(dòng)排序的問(wèn)題,筆者在github上找到一個(gè)相關(guān)資料moayes/UDo,但是發(fā)現(xiàn)它有幾個(gè)缺點(diǎn),比如:

1.不支持分組,只有一組的情況下才能拖動(dòng)排序
2.cell拖動(dòng)到tableView頂端或底部的時(shí)候,tableView不會(huì)自動(dòng)滾動(dòng)
3.只能插入不能交換,即將第1個(gè)cell移動(dòng)到第5個(gè),我希望結(jié)果是52341,而實(shí)際卻是23451

于是筆者利用業(yè)余時(shí)間,自己寫了一個(gè)可以通過(guò)長(zhǎng)按cell來(lái)拖動(dòng)進(jìn)行排序與分組的tableView,這里大概說(shuō)一下筆者的思路,需要詳細(xì)代碼的看這里,感謝moayes/UDo的思路

效果圖

實(shí)現(xiàn)思路

首先給tableView添加一個(gè)長(zhǎng)按手勢(shì),為什么給tableView添加,而不是給cell添加?

1.無(wú)論手勢(shì)添加到tableView上還是cell上,長(zhǎng)按cell的時(shí)候都會(huì)響應(yīng)事件
2.tableView只需要添加一次,而cell每一個(gè)都要添加,效率太低
3.如果添加在cell上,當(dāng)tableView通過(guò)點(diǎn)擊的坐標(biāo)獲取indexPath時(shí),還需要將坐標(biāo)從cell上轉(zhuǎn)換到tableView上

綜上所述,手勢(shì)添加在tableView上。

1.cell拖動(dòng)的幾種情況

例如我將第一個(gè)cell移動(dòng)到第五個(gè)時(shí),可以分為兩種情況
1.插入:23451
2.交換:52341

插入時(shí),當(dāng)1經(jīng)過(guò)2,應(yīng)將2前移一個(gè)位置,經(jīng)過(guò)3,將3前移一個(gè)位置,以此類推。所以在拖拽過(guò)程中就應(yīng)該改變2345的位置,因此插入操作在UIGestureRecognizerStateChanged中進(jìn)行

交換時(shí),由于最終只有1跟5的位置發(fā)生變化,所以交換操作在UIGestureRecognizerStateEnded中進(jìn)行即可

2.cell拖動(dòng)的實(shí)現(xiàn)

其實(shí)我們看到的被拖動(dòng)的cell并不是真正的cell,只不過(guò)是一個(gè)跟cell長(zhǎng)得一模一樣的圖片罷了。

當(dāng)長(zhǎng)按cell時(shí),開(kāi)啟圖形上下文,將cell.layer渲染到上下文中,然后獲取上下文中的圖片,顯示在一個(gè)imageView上,并將imageView添加到tableView上覆蓋cell,之后要拖動(dòng)的就是這個(gè)imageView

//獲取點(diǎn)擊的位置
CGPoint point = [sender locationInView:self];
if (sender.state == UIGestureRecognizerStateBegan) {
    //根據(jù)手勢(shì)點(diǎn)擊的位置,獲取被點(diǎn)擊cell所在的indexPath
    self.fromIndexPath = [self indexPathForRowAtPoint:point];
    
    //不一定能獲取到indexPath,因?yàn)辄c(diǎn)擊的位置可能是header或者footer
    if (!_fromIndexPath) return;

    //根據(jù)indexPath獲取cell
    UITableViewCell *cell = [self cellForRowAtIndexPath:_fromIndexPath];
    
    //創(chuàng)建一個(gè)imageView,imageView的image由cell渲染得來(lái)
    self.cellImageView = [self createCellImageView:cell];
    
    //更改imageView的中心點(diǎn)為手指點(diǎn)擊位置
    CGPoint center = cell.center;
    self.cellImageView.center = center;
    
    //隱藏要移動(dòng)的cell
    cell.hidden = YES;
} 

當(dāng)手指在屏幕上移動(dòng)時(shí),我們需要做三件事

1.實(shí)時(shí)改變imageView中心點(diǎn)的y值為手指所點(diǎn)擊坐標(biāo)的y值
2.如果imageView被拖到了tableView的頂部或者底部,讓tableView自動(dòng)滾動(dòng)
3.如果是插入效果,改變所經(jīng)過(guò)cell的位置

if (sender.state == UIGestureRecognizerStateChanged){
    _toIndexPath = [self indexPathForRowAtPoint:point];
    
    //1.更改imageView的中心點(diǎn)為手指點(diǎn)擊位置
    CGPoint center = self.cellImageView.center;
    center.y = point.y;
    self.cellImageView.center = center;

    //2.判斷cell是否被拖拽到了tableView的邊緣,如果是,則自動(dòng)滾動(dòng)tableView
    if ([self isScrollToEdge]) {
        [self startTimerToScrollTableView];
    } else {
        [_displayLink invalidate];
    }

    //3.如果是插入效果,實(shí)時(shí)改變所經(jīng)過(guò)cell的位置
    if (_toIndexPath && ![_toIndexPath isEqual:_fromIndexPath] && !self.isExchange)
        [self insertCell:_toIndexPath];
}
如何判斷imageView是否拖動(dòng)到了tableView的底部或者頂端

一張圖告訴你

當(dāng)拖動(dòng)到底部或頂端時(shí),我們開(kāi)啟一個(gè)定時(shí)器,通過(guò)改變tableView的偏移量來(lái)實(shí)現(xiàn)tableView的自動(dòng)滾動(dòng),那何時(shí)停止這個(gè)定時(shí)器呢?這里又要分兩種情況。

1.當(dāng)imageView被拖走,不在邊緣的時(shí)候
2.當(dāng)tableView已經(jīng)滾動(dòng)到最頂部或者最底部的時(shí)候

對(duì)于第一種情況,筆者不做闡述,對(duì)于第二種情況,應(yīng)該如何判斷呢?
依舊一張圖告訴你

判斷是否滾動(dòng)到邊緣的代碼
- (BOOL)isScrollToEdge {
if ((CGRectGetMaxY(self.cellImageView.frame) > self.contentOffset.y + self.frame.size.height - self.contentInset.bottom) && (self.contentOffset.y < self.contentSize.height - self.frame.size.height + self.contentInset.bottom)) {
self.autoScroll = AutoScrollDown;
return YES;
}

    if ((self.cellImageView.frame.origin.y < self.contentOffset.y + self.contentInset.top) && (self.contentOffset.y > -self.contentInset.top)) {
        self.autoScroll = AutoScrollUp;
        return YES;
    }
    return NO;
}
設(shè)置tableView.contentOffset讓tableView自動(dòng)滾動(dòng)時(shí)的注意點(diǎn)

當(dāng)imageView移動(dòng)到tableView的底部或者頂部時(shí),如果手指不再移動(dòng),那么長(zhǎng)按手勢(shì)的方法也不會(huì)調(diào)用,這就需要在改變tableView.contentOffset的同時(shí)改變imageView的位置,而且每經(jīng)過(guò)一個(gè)cell,都要執(zhí)行一次插入操作

- (void)scrollTableView{
   //如果已經(jīng)滾動(dòng)到最上面或最下面,則停止定時(shí)器并返回
    if ((_autoScroll == AutoScrollUp && self.contentOffset.y <= -self.contentInset.top)
    || (_autoScroll == AutoScrollDown && self.contentOffset.y >= self.contentSize.height - self.frame.size.height + self.contentInset.bottom)) {
            [_displayLink invalidate];
            return;
    }

    //改變tableView的contentOffset,實(shí)現(xiàn)自動(dòng)滾動(dòng)
    CGFloat height = _autoScroll == AutoScrollUp? -_scrollSpeed : _scrollSpeed;
    [self setContentOffset:CGPointMake(0, self.contentOffset.y + height)];
    //改變cellImageView的位置為手指所在位置
    _cellImageView.center = CGPointMake(_cellImageView.center.x, _cellImageView.center.y + height);

    //滾動(dòng)tableView的同時(shí)也要執(zhí)行插入操作
    _toIndexPath = [self indexPathForRowAtPoint:_cellImageView.center];
    if (_toIndexPath && ![_toIndexPath isEqual:_fromIndexPath] && !self.isExchange)
        [self insertCell:_toIndexPath];
}
如何執(zhí)行插入操作

其實(shí)非常簡(jiǎn)單,改變模型在數(shù)組中的位置,刷新tableView即可,當(dāng)然這也要分兩種情況,tableView是否有分組。需要注意的就是,刷新以后,之前隱藏的cell會(huì)顯示出來(lái),這時(shí)我們需要重新隱藏
- (void)insertCell:(NSIndexPath *)toIndexPath {
if (self.isGroup) {//有分組的情況
//先將cell的數(shù)據(jù)模型從之前的數(shù)組中移除,然后再插入新的數(shù)組
NSMutableArray *fromSection = self.dataArray[_fromIndexPath.section];
NSMutableArray *toSection = self.dataArray[toIndexPath.section];
id obj = fromSection[_fromIndexPath.row];
[fromSection removeObject:obj];
[toSection insertObject:obj atIndex:toIndexPath.row];

        //如果某個(gè)組的所有cell都被移動(dòng)到其他組,則刪除這個(gè)組
        if (!fromSection.count) {
            [self.dataArray removeObject:fromSection];
        }
    } else {//沒(méi)有分組的情況
        //交換兩個(gè)cell的數(shù)據(jù)模型
        [self.dataArray exchangeObjectAtIndex:_fromIndexPath.row withObjectAtIndex:toIndexPath.row];
    }
    [self reloadData];

    //重新隱藏cell
    UITableViewCell *cell = [self cellForRowAtIndexPath:toIndexPath];
    cell.hidden = YES;
    _fromIndexPath = toIndexPath;
}

當(dāng)長(zhǎng)按操作結(jié)束時(shí),停止定時(shí)器,移除imageView,并將隱藏的cell顯示出來(lái),如果是交換方式的話,還需要在此執(zhí)行交換操作

 if (sender.state == UIGestureRecognizerStateEnded){
    if (self.isExchange) [self exchangeCell:point];
    [_displayLink invalidate];
    //將隱藏的cell顯示出來(lái),并將imageView移除掉
    UITableViewCell *cell = [self cellForRowAtIndexPath:_fromIndexPath];
    cell.hidden = NO;
    [self.cellImageView removeFromSuperview];
}
如何執(zhí)行交換操作

與插入一樣,改變模型在數(shù)組中的位置,刷新tableView即可
- (void)exchangeCell:(CGPoint)point {
NSIndexPath *toIndexPath = [self indexPathForRowAtPoint:point];
if (!toIndexPath) return;
//交換要移動(dòng)cell與被替換cell的數(shù)據(jù)模型
if (self.isGroup) {
//分組情況下,交換模型的過(guò)程比較復(fù)雜
NSMutableArray *fromSection = self.dataArray[_fromIndexPath.section];
NSMutableArray *toSection = self.dataArray[toIndexPath.section];
id obj = fromSection[_fromIndexPath.row];
[fromSection replaceObjectAtIndex:_fromIndexPath.row withObject:toSection[toIndexPath.row]];
[toSection replaceObjectAtIndex:toIndexPath.row withObject:obj];
} else {
[self.dataArray exchangeObjectAtIndex:_fromIndexPath.row withObjectAtIndex:toIndexPath.row];
}
[self reloadData];
}

結(jié)束語(yǔ)

以上是筆者的主要思路以及核心代碼,需要全部代碼的請(qǐng)前往github下載,https://github.com/codingZero/XRDragTableView,如果覺(jué)得對(duì)你有幫助,那就動(dòng)動(dòng)手指,star一下吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容