寫在前面
這段時(shí)間都在忙新項(xiàng)目的事兒,沒有時(shí)間倒騰,這兩天閑下來,想著一直沒有細(xì)細(xì)的研究CollectionView,一般最多用來做點(diǎn)循環(huán)滾動(dòng),所以花時(shí)間深入學(xué)習(xí)了一些東西,這次實(shí)現(xiàn)了CollectionView的拖動(dòng)重排的效果,先請看圖:(吐槽:不知道為啥從xcode7開始,模擬器變得很卡很卡,所以截圖的效果不好,大家可以在真機(jī)上測試,效果還是非常不錯(cuò)的)
2月27日更新:
修復(fù)了拖拽滾動(dòng)時(shí)抖動(dòng)的一個(gè)bug,新增編輯模式,進(jìn)入編輯模式后不用長按觸發(fā)手勢,且在開啟抖動(dòng)的情況下會自動(dòng)進(jìn)入抖動(dòng)模式,如圖:
圖1:垂直滾動(dòng)
圖2:水平滾動(dòng)
圖3:配合瀑布流(我直接使用了上個(gè)項(xiàng)目的瀑布流模塊做了集成實(shí)驗(yàn))
我將整個(gè)控件進(jìn)行了封裝,名字是
XWDragCellCollectionView
使用起來非常方便,github地址:可拖拽重排的CollectionView;使用也非常簡單,只需3步,步驟如下:
1、繼承于XWDragCellCollectionView;
2、實(shí)現(xiàn)必須實(shí)現(xiàn)的DataSouce代理方法:(在該方法中返回整個(gè)CollectionView的數(shù)據(jù)數(shù)組用于重排)
- (NSArray *)dataSourceArrayOfCollectionView:(XWDragCellCollectionView *)collectionView;
3、實(shí)現(xiàn)必須實(shí)現(xiàn)的一個(gè)Delegate代理方法:(在該方法中將重拍好的新數(shù)據(jù)源設(shè)為當(dāng)前數(shù)據(jù)源)(例如 :_data = newDataArray)
- (void)dragCellCollectionView:(XWDragCellCollectionView *)collectionView newDataArrayAfterMove:(NSArray *)newDataArray;
詳細(xì)的使用可以查看代碼中的demo,支持設(shè)置長按事件,是否開啟邊緣滑動(dòng),抖動(dòng)、以及設(shè)置抖動(dòng)等級,這些在h文件里面都有詳細(xì)說明,有需要的可以嘗試一下,并多多提意見,作為新手,肯定還有很多不足的地方;
原理
在剛剛考慮這個(gè)效果的時(shí)候,我仔細(xì)分析了一下效果,我首先想到的就是利用截圖大法,將手指要移動(dòng)的cell截個(gè)圖來進(jìn)行移動(dòng),并隱藏該cell,然后在合適的時(shí)候交換cell的位置,造成是拖拽cell被拖拽到新位置的效果,我將主要實(shí)現(xiàn)的步驟分為如下步驟:
1、給CollectionView添加一個(gè)長按手勢,用于效果驅(qū)動(dòng)
- (void)xwp_addGesture{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(xwp_longPressed:)];
_longPressGesture = longPress;
//設(shè)置長按時(shí)間
longPress.minimumPressDuration = _minimumPressDuration;
[self addGestureRecognizer:longPress];
}
2、在手勢開始的時(shí)候,得到手指所在的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;
//記錄當(dāng)前手指位置
_lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
}
3、在手勢移動(dòng)的時(shí)候,計(jì)算出手勢移動(dòng)的距離,并移動(dòng)截圖視圖,當(dāng)截圖視圖于某一個(gè)cell(可見cell)相交到一定程度的時(shí)候,我就讓調(diào)用系統(tǒng)的api交換這個(gè)cell和隱藏cell的位置,形成動(dòng)畫,同時(shí)更新數(shù)據(jù)源(更新數(shù)據(jù)源是最重要的操作!)
- (void)xwp_gestureChange:(UILongPressGestureRecognizer *)longPressGesture{
//計(jì)算移動(dòng)距離
CGFloat tranX = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].x - _lastPoint.x;
CGFloat tranY = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].y - _lastPoint.y;
//設(shè)置截圖視圖位置
_tempMoveCell.center = CGPointApplyAffineTransform(_tempMoveCell.center, CGAffineTransformMakeTranslation(tranX, tranY));
_lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
//計(jì)算截圖視圖和哪個(gè)cell相交
for (UICollectionViewCell *cell in [self visibleCells]) {
//剔除隱藏的cell
if ([self indexPathForCell:cell] == _originalIndexPath) {
continue;
}
//計(jì)算中心距
CGFloat space = sqrtf(pow(_tempMoveCell.center.x - cell.center.x, 2) + powf(_tempMoveCell.center.y - cell.center.y, 2));
//如果相交一半就移動(dòng)
if (space <= _tempMoveCell.bounds.size.width / 2) {
_moveIndexPath = [self indexPathForCell:cell];
//更新數(shù)據(jù)源(移動(dòng)前必須更新數(shù)據(jù)源)
[self xwp_updateDataSource];
//移動(dòng)
[self moveItemAtIndexPath:_originalIndexPath toIndexPath:_moveIndexPath];
//通知代理
//設(shè)置移動(dòng)后的起始indexPath
_originalIndexPath = _moveIndexPath;
break;
}
}
}
/**
* 更新數(shù)據(jù)源
*/
- (void)xwp_updateDataSource{
NSMutableArray *temp = @[].mutableCopy;
//通過代理獲取數(shù)據(jù)源,該代理方法必須實(shí)現(xiàn)
if ([self.dataSource respondsToSelector:@selector(dataSourceArrayOfCollectionView:)]) {
[temp addObjectsFromArray:[self.dataSource dataSourceArrayOfCollectionView:self]];
}
//判斷數(shù)據(jù)源是單個(gè)數(shù)組還是數(shù)組套數(shù)組的多section形式,YES表示數(shù)組套數(shù)組
BOOL dataTypeCheck = ([self numberOfSections] != 1 || ([self numberOfSections] == 1 && [temp[0] isKindOfClass:[NSArray class]]));
//先將數(shù)據(jù)源的數(shù)組都變?yōu)榭勺償?shù)據(jù)方便操作
if (dataTypeCheck) {
for (int i = 0; i < temp.count; i ++) {
[temp replaceObjectAtIndex:i withObject:[temp[i] mutableCopy]];
}
}
if (_moveIndexPath.section == _originalIndexPath.section) {
//在同一個(gè)section中移動(dòng)或者只有一個(gè)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之間移動(dòng)的情況(原理是刪除原位置所在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]];
}
//將重排好的數(shù)據(jù)傳遞給外部,在外部設(shè)置新的數(shù)據(jù)源,該代理方法必須實(shí)現(xiàn)
if ([self.delegate respondsToSelector:@selector(dragCellCollectionView:newDataArrayAfterMove:)]) {
[self.delegate dragCellCollectionView:self newDataArrayAfterMove:temp.copy];
}
}
4、手勢結(jié)束的時(shí)候?qū)⒔貓D視圖動(dòng)畫移動(dòng)到隱藏cell所在位置,并顯示隱藏cell并移除截圖視圖;
- (void)xwp_gestureEndOrCancle:(UILongPressGestureRecognizer *)longPressGesture{
UICollectionViewCell *cell = [self cellForItemAtIndexPath:_originalIndexPath];
//結(jié)束動(dòng)畫過程中停止交互,防止出問題
self.userInteractionEnabled = NO;
//給截圖視圖一個(gè)動(dòng)畫移動(dòng)到隱藏cell的新位置
[UIView animateWithDuration:0.25 animations:^{
_tempMoveCell.center = cell.center;
} completion:^(BOOL finished) {
//移除截圖視圖、顯示隱藏cell并開啟交互
[_tempMoveCell removeFromSuperview];
cell.hidden = NO;
self.userInteractionEnabled = YES;
}];
}
關(guān)鍵效果的代碼就是上面這些了,還有寫細(xì)節(jié)的東西請大家自行查看源代碼
寫在最后
從iOS9開始,系統(tǒng)已經(jīng)提供了重排的API,不用我們這么辛苦的自己寫,不過想要只適配iOS9,還有一段時(shí)間,不過大家可以嘗試去實(shí)現(xiàn)以下這幾個(gè)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);
接下來,還準(zhǔn)備研究一下CollectionView的轉(zhuǎn)場和自定義布局,已經(jīng)寫了一些自定義布局效果了,總結(jié)好了再貼出來,CollectionView實(shí)在是一枚非常強(qiáng)大的控件,大家都應(yīng)該去深入的研究一下,說不定會產(chǎn)生許多奇妙的想法!加油咯!最后復(fù)習(xí)一下github地址:可拖拽重排的CollectionView,如果覺得有幫助,請給與一顆star鼓勵(lì)一下,謝謝!