可拖拽重排的CollectionView

寫在前面

這段時間都在忙新項目的事兒,沒有時間倒騰,這兩天閑下來,想著一直沒有細細的研究CollectionView,一般最多用來做點循環滾動,所以花時間深入學習了一些東西,這次實現了CollectionView的拖動重排的效果,先請看圖:(吐槽:不知道為啥從xcode7開始,模擬器變得很卡很卡,所以截圖的效果不好,大家可以在真機上測試,效果還是非常不錯的)

2月27日更新:

修復了拖拽滾動時抖動的一個bug,新增編輯模式,進入編輯模式后不用長按觸發手勢,且在開啟抖動的情況下會自動進入抖動模式,如圖:


test.gif

圖1:垂直滾動

drag1.gif

圖2:水平滾動

drag2.gif

圖3:配合瀑布流(我直接使用了上個項目的瀑布流模塊做了集成實驗)

drag5.gif

我將整個控件進行了封裝,名字是XWDragCellCollectionView使用起來非常方便,github地址:可拖拽重排的CollectionView;使用也非常簡單,只需3步,步驟如下:

1、繼承于XWDragCellCollectionView;

2、實現必須實現的DataSouce代理方法:(在該方法中返回整個CollectionView的數據數組用于重排)
    - (NSArray *)dataSourceArrayOfCollectionView:(XWDragCellCollectionView *)collectionView;
    
3、實現必須實現的一個Delegate代理方法:(在該方法中將重拍好的新數據源設為當前數據源)(例如 :_data = newDataArray)
    - (void)dragCellCollectionView:(XWDragCellCollectionView *)collectionView newDataArrayAfterMove:(NSArray *)newDataArray;
    

詳細的使用可以查看代碼中的demo,支持設置長按事件,是否開啟邊緣滑動,抖動、以及設置抖動等級,這些在h文件里面都有詳細說明,有需要的可以嘗試一下,并多多提意見,作為新手,肯定還有很多不足的地方;

原理

在剛剛考慮這個效果的時候,我仔細分析了一下效果,我首先想到的就是利用截圖大法,將手指要移動的cell截個圖來進行移動,并隱藏該cell,然后在合適的時候交換cell的位置,造成是拖拽cell被拖拽到新位置的效果,我將主要實現的步驟分為如下步驟:

1、給CollectionView添加一個長按手勢,用于效果驅動

- (void)xwp_addGesture{
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(xwp_longPressed:)];
    _longPressGesture = longPress;
    //設置長按時間
    longPress.minimumPressDuration = _minimumPressDuration;
    [self addGestureRecognizer:longPress];
}

2、在手勢開始的時候,得到手指所在的cell,并截圖,并將原有cell隱藏

- (void)xwp_gestureBegan:(UILongPressGestureRecognizer *)longPressGesture{
    //獲取手指所在的cell
    _originalIndexPath = [self indexPathForItemAtPoint:[longPressGesture locationOfTouch:0 inView:longPressGesture.view]];
    UICollectionViewCell *cell = [self cellForItemAtIndexPath:_originalIndexPath];
    //截圖大法,得到cell的截圖視圖
    UIView *tempMoveCell = [cell snapshotViewAfterScreenUpdates:NO];
    _tempMoveCell = tempMoveCell;
    _tempMoveCell.frame = cell.frame;
    [self addSubview:_tempMoveCell];
    //隱藏cell
    cell.hidden = YES;
    //記錄當前手指位置
    _lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
}

3、在手勢移動的時候,計算出手勢移動的距離,并移動截圖視圖,當截圖視圖于某一個cell(可見cell)相交到一定程度的時候,我就讓調用系統的api交換這個cell和隱藏cell的位置,形成動畫,同時更新數據源(更新數據源是最重要的操作!)

- (void)xwp_gestureChange:(UILongPressGestureRecognizer *)longPressGesture{
    //計算移動距離
    CGFloat tranX = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].x - _lastPoint.x;
    CGFloat tranY = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].y - _lastPoint.y;
    //設置截圖視圖位置
    _tempMoveCell.center = CGPointApplyAffineTransform(_tempMoveCell.center, CGAffineTransformMakeTranslation(tranX, tranY));
    _lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
    //計算截圖視圖和哪個cell相交
    for (UICollectionViewCell *cell in [self visibleCells]) {
        //剔除隱藏的cell
        if ([self indexPathForCell:cell] == _originalIndexPath) {
            continue;
        }
        //計算中心距
        CGFloat space = sqrtf(pow(_tempMoveCell.center.x - cell.center.x, 2) + powf(_tempMoveCell.center.y - cell.center.y, 2));
        //如果相交一半就移動
        if (space <= _tempMoveCell.bounds.size.width / 2) {
            _moveIndexPath = [self indexPathForCell:cell];
            //更新數據源(移動前必須更新數據源)
            [self xwp_updateDataSource];
            //移動
            [self moveItemAtIndexPath:_originalIndexPath toIndexPath:_moveIndexPath];
            //通知代理
            //設置移動后的起始indexPath
            _originalIndexPath = _moveIndexPath;
            break;
        }
    }
}
/**
 *  更新數據源
 */
- (void)xwp_updateDataSource{
    NSMutableArray *temp = @[].mutableCopy;
    //通過代理獲取數據源,該代理方法必須實現
    if ([self.dataSource respondsToSelector:@selector(dataSourceArrayOfCollectionView:)]) {
        [temp addObjectsFromArray:[self.dataSource dataSourceArrayOfCollectionView:self]];
    }
    //判斷數據源是單個數組還是數組套數組的多section形式,YES表示數組套數組
    BOOL dataTypeCheck = ([self numberOfSections] != 1 || ([self numberOfSections] == 1 && [temp[0] isKindOfClass:[NSArray class]]));
    //先將數據源的數組都變為可變數據方便操作
    if (dataTypeCheck) {
        for (int i = 0; i < temp.count; i ++) {
            [temp replaceObjectAtIndex:i withObject:[temp[i] mutableCopy]];
        }
    }
    if (_moveIndexPath.section == _originalIndexPath.section) {
    //在同一個section中移動或者只有一個section的情況(原理就是將原位置和新位置之間的cell向前或者向后平移)
        NSMutableArray *orignalSection = dataTypeCheck ? temp[_originalIndexPath.section] : temp;
        if (_moveIndexPath.item > _originalIndexPath.item) {
            for (NSUInteger i = _originalIndexPath.item; i < _moveIndexPath.item ; i ++) {
                [orignalSection exchangeObjectAtIndex:i withObjectAtIndex:i + 1];
            }
        }else{
            for (NSUInteger i = _originalIndexPath.item; i > _moveIndexPath.item ; i --) {
                [orignalSection exchangeObjectAtIndex:i withObjectAtIndex:i - 1];
            }
        }
    }else{
    //在不同section之間移動的情況(原理是刪除原位置所在section的cell并插入到新位置所在的section中)
        NSMutableArray *orignalSection = temp[_originalIndexPath.section];
        NSMutableArray *currentSection = temp[_moveIndexPath.section];
        [currentSection insertObject:orignalSection[_originalIndexPath.item] atIndex:_moveIndexPath.item];
        [orignalSection removeObject:orignalSection[_originalIndexPath.item]];
    }
    //將重排好的數據傳遞給外部,在外部設置新的數據源,該代理方法必須實現
    if ([self.delegate respondsToSelector:@selector(dragCellCollectionView:newDataArrayAfterMove:)]) {
        [self.delegate dragCellCollectionView:self newDataArrayAfterMove:temp.copy];
    }
}

4、手勢結束的時候將截圖視圖動畫移動到隱藏cell所在位置,并顯示隱藏cell并移除截圖視圖;

- (void)xwp_gestureEndOrCancle:(UILongPressGestureRecognizer *)longPressGesture{
    UICollectionViewCell *cell = [self cellForItemAtIndexPath:_originalIndexPath];
    //結束動畫過程中停止交互,防止出問題
    self.userInteractionEnabled = NO;
    //給截圖視圖一個動畫移動到隱藏cell的新位置
    [UIView animateWithDuration:0.25 animations:^{
        _tempMoveCell.center = cell.center;
    } completion:^(BOOL finished) {
        //移除截圖視圖、顯示隱藏cell并開啟交互
        [_tempMoveCell removeFromSuperview];
        cell.hidden = NO;
        self.userInteractionEnabled = YES;
    }];
}

關鍵效果的代碼就是上面這些了,還有寫細節的東西請大家自行查看源代碼

寫在最后

從iOS9開始,系統已經提供了重排的API,不用我們這么辛苦的自己寫,不過想要只適配iOS9,還有一段時間,不過大家可以嘗試去實現以下這幾個API:

// Support for reordering
- (BOOL)beginInteractiveMovementForItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0); // returns NO if reordering was prevented from beginning - otherwise YES
- (void)updateInteractiveMovementTargetPosition:(CGPoint)targetPosition NS_AVAILABLE_IOS(9_0);
- (void)endInteractiveMovement NS_AVAILABLE_IOS(9_0);
- (void)cancelInteractiveMovement NS_AVAILABLE_IOS(9_0);

接下來,還準備研究一下CollectionView的轉場和自定義布局,已經寫了一些自定義布局效果了,總結好了再貼出來,CollectionView實在是一枚非常強大的控件,大家都應該去深入的研究一下,說不定會產生許多奇妙的想法!加油咯!最后復習一下github地址:可拖拽重排的CollectionView,如果覺得有幫助,請給與一顆star鼓勵一下,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容