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

圖1:垂直滾動

圖2:水平滾動

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

我將整個控件進行了封裝,名字是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鼓勵一下,謝謝!
原文鏈接:http://www.jianshu.com/p/8f0153ce17f9
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。