在UIcollectionView中實(shí)現(xiàn)類似電腦資源管理器里的那種將文件拖入圖標(biāo)就可以完成添加操作的效果,如圖:

這個(gè)gif經(jīng)過壓縮,效果不太好。實(shí)際效果比圖上順滑很多。
首先我下載了 https://github.com/wazrx/XWDragCellCollectionView
這個(gè)控件作為基礎(chǔ),感謝作者!
XWDragCellCollectionView可以實(shí)現(xiàn)垂直/水平的滾動以及滑動排序,這個(gè)不是研究的重點(diǎn),我就不重復(fù)造輪子,而是在它的基礎(chǔ)上來改出我們想要的功能。
首先添加手勢,用長按手勢激活cell,來進(jìn)行接下來的操作:
- (void)initUI {
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) collectionViewLayout:flowLayout];
self.collectionView = collectionView;
[self.view addSubview:collectionView];
collectionView.delegate = self;
collectionView.dataSource = self;
[collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:identity];
collectionView.alwaysBounceVertical = YES;
UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongGesture:)];
[collectionView addGestureRecognizer:longGesture];
}
這是長按手勢所要激活的方法:
- (void)handleLongGesture:(UILongPressGestureRecognizer *)longGesture {
switch (longGesture.state) {
case UIGestureRecognizerStateBegan:
[self gestureBegan:longGesture];
break;
case UIGestureRecognizerStateChanged:
[self gestureChange:longGesture];
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
[self gestureEndOrCancle:longGesture];
break;
default:
break;
}
}
方法中分別對操作的開始、拖動和結(jié)束/取消做了處理,先看開始的方法:
- (void)gestureBegan:(UILongPressGestureRecognizer *)longPressGesture {
self.originalIndexPath = [self.collectionView indexPathForItemAtPoint:[longPressGesture locationOfTouch:0 inView:longPressGesture.view]];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:self.originalIndexPath];//拿到被長按的格子
//下面這一片是對這個(gè)格子截圖
UIImage *snap;
UIGraphicsBeginImageContextWithOptions(cell.bounds.size, 1.0f, 0);
[cell.layer renderInContext:UIGraphicsGetCurrentContext()];
snap = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//把截好的圖片裝進(jìn)一個(gè)假的View里,之后用這個(gè)View隨手指拖動而運(yùn)動而隱藏原格子,給用戶造成其實(shí)是格子被拖動的效果
UIView *tempMoveCell = [UIView new];
tempMoveCell.layer.contents = (__bridge id)snap.CGImage;
cell.hidden = YES;
self.orignalCell = cell;
self.orignalCenter = cell.center;
self.tempMoveCell = tempMoveCell;
self.tempMoveCell.frame = cell.frame;
[self.collectionView addSubview:self.tempMoveCell];
//開啟邊緣滾動定時(shí)器
[self setEdgeTimer];
//開啟抖動,和原控件不同,我這個(gè)是只抖兩下
[self itemshake];
self.lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
}
- (void)itemshake {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
[animation setDuration:0.1];
animation.fromValue = @(-M_1_PI/6);
animation.toValue = @(M_1_PI/6);
animation.repeatCount = 1;
animation.autoreverses = YES;
self.tempMoveCell.layer.anchorPoint = CGPointMake(0.5, 0.5);
[self.tempMoveCell.layer addAnimation:animation forKey:@"rotation"];
}
接下來是重點(diǎn)的拖動時(shí)候調(diào)用的方法:
- (void)gestureChange:(UILongPressGestureRecognizer *)longPressGesture {
CGFloat tranX = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].x - self.lastPoint.x;
CGFloat tranY = [longPressGesture locationOfTouch:0 inView:longPressGesture.view].y - self.lastPoint.y;
self.tempMoveCell.center = CGPointApplyAffineTransform(self.tempMoveCell.center, CGAffineTransformMakeTranslation(tranX, tranY));
//讓你的假格子View隨手指移動
self.lastPoint = [longPressGesture locationOfTouch:0 inView:longPressGesture.view];
[self handleCell];
}
handleCell方法則負(fù)責(zé)分辨具體的操作
#define MoveSpace 20 //在格子里的這么大范圍內(nèi)也算move操作的觸發(fā)點(diǎn),而不是add操作的觸發(fā)點(diǎn)
- (void)handleCell {
for (UICollectionViewCell *cell in [self.collectionView visibleCells]) {//遍歷所有的可視cell
if ([self.collectionView indexPathForCell:cell] == _originalIndexPath) {//如果是自己這個(gè)cell,那么跳過
continue;
}
//計(jì)算所有表格中心和在移動的格子中心的距離
CGFloat spacingX = fabs(self.tempMoveCell.center.x - cell.center.x);
CGFloat spacingY = fabs(_tempMoveCell.center.y - cell.center.y);
if (self.motherCell == cell) {
if (spacingX > _tempMoveCell.bounds.size.width / 2.0f - MoveSpace || spacingY > _tempMoveCell.bounds.size.height / 2.0f - MoveSpace) {
//跑出了格子
[self stopAddToCellWithData:NO];
}
}
if (spacingX <= _tempMoveCell.bounds.size.width / 2.0f - MoveSpace && spacingY <= _tempMoveCell.bounds.size.height / 2.0f - MoveSpace) {
// NSLog(@"進(jìn)入格子內(nèi)");
self.motherCell = cell;
[self setAddTimer];
self.moveIndexPath = [self.collectionView indexPathForCell:cell];
} else if (spacingX >= _tempMoveCell.bounds.size.width / 2.0f - MoveSpace && spacingX <= _tempMoveCell.bounds.size.width / 2.0f + 7.5 && spacingY <= _tempMoveCell.bounds.size.height / 2.0f - MoveSpace) {
//移動
self.willMoveCell = cell;
[self setMoveTimer];
break;
}
}
}
看代碼到這里,你可能會發(fā)現(xiàn)setMoveTimer、setAddTimer 和 setEdgeTimer三個(gè)方法我并沒有解釋,這三個(gè)是計(jì)時(shí)器,用來延遲用戶操作,提升手感。比如setMoveTimer
- (void)setMoveTimer {
if (!_moveTimer) {
[self stopAddTimer];
// NSLog(@"新建timer");
_moveTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(moveCell) userInfo:nil repeats:NO];
}
}
- (void)stopMoveTimer {
if (_moveTimer) {
// NSLog(@"銷毀timer");
[_moveTimer invalidate];
_moveTimer = nil;
}
}
他規(guī)定了用戶將格子移動到會觸發(fā)move(排序)操作的位置時(shí)有0.5秒鐘的緩沖而setAddTimer:
- (void)setAddTimer {
if (!_addTimer) {
[self stopMoveTimer];
// NSLog(@"新建timer");
_addTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(addToCell) userInfo:nil repeats:NO];
}
}
- (void)stopAddTimer {
if (_addTimer) {
// NSLog(@"銷毀timer");
[_addTimer invalidate];
_addTimer = nil;
}
}
則讓add操作有1秒的緩沖,同時(shí)這兩個(gè)計(jì)時(shí)器在生成時(shí)都會ban掉對方,防止操作混亂。
setEdgeTimer是負(fù)責(zé)拖動按鈕移動到屏幕邊緣觸發(fā)滾動的。這個(gè)是原控件里就有的方法,不作講解。
在移動計(jì)時(shí)器到時(shí)間后,就會觸發(fā)move操作,此邏輯主要來自原控件
- (void)moveCell {
NSLog(@"%@", [self.collectionView cellForItemAtIndexPath:self.originalIndexPath]);
self.moveIndexPath = [self.collectionView indexPathForCell:self.willMoveCell];
self.orignalCell = self.willMoveCell;
self.orignalCenter = self.willMoveCell.center;
[CATransaction begin];
[self.collectionView moveItemAtIndexPath:self.originalIndexPath toIndexPath:self.moveIndexPath];
[CATransaction setCompletionBlock:^{
// NSLog(@"動畫完成");
[self stopMoveTimer];
}];
[CATransaction commit];
self.originalIndexPath = self.moveIndexPath;
}
而add操作則分兩步:開始add和結(jié)束add。結(jié)束add又分兩種情況:完成add操作和取消add操作。完成add操作要對文件進(jìn)行更改,取消add操作要將表格還原到操作之前的狀態(tài)。首先看開始add:
- (void)addToCell {
// NSLog(@"開始add操作");
if (self.motherCell) {
// NSLog(@"開始放大");
//跟上面一樣,對添加操作作為文件夾一方的格子(motherCell)進(jìn)行截圖
UIImage *snap;
UIGraphicsBeginImageContextWithOptions(self.motherCell.bounds.size, 1.0f, 0);
[self.motherCell.layer renderInContext:UIGraphicsGetCurrentContext()];
snap = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//把截圖裝進(jìn)一個(gè)假的View里
UIView *bigMotherCell = [UIView new];
bigMotherCell.layer.contents = (__bridge id)snap.CGImage;
// self.motherCell.hidden = YES;
self.bigMotherCell = bigMotherCell;
self.bigMotherCell.frame = self.motherCell.frame;
[self.collectionView addSubview:self.bigMotherCell];
CGRect rect = self.bigMotherCell.frame;
CGFloat scale = 1.3;
[self.collectionView bringSubviewToFront:self.tempMoveCell];
//方法這個(gè)假View,造成文件夾要容納文件的效果
[UIView animateWithDuration:0.5 animations:^{
self.bigMotherCell.frame = CGRectMake(rect.origin.x - rect.size.width * (scale - 1)/2, rect.origin.y - rect.size.height * (scale - 1)/2, rect.size.width * scale, rect.size.height * scale);
} completion:nil];
} else {
// NSLog(@"沒有motherCell");
}
}
結(jié)束add的兩種情況我選擇用一個(gè)bool值進(jìn)行區(qū)分,yes是完成操作,no是取消操作
- (void)stopAddToCellWithData:(BOOL)withData {
if (self.motherCell) {
[UIView animateWithDuration:0.1 animations:^{
self.bigMotherCell.frame = self.motherCell.frame;
if (withData) {
// NSIndexPath *motherIndexPath = [self.collectionView indexPathForCell:self.motherCell];
NSIndexPath *childIndexPath = [self.collectionView indexPathForCell:self.orignalCell];
NSLog(@"把編號為%@的格子移動到編號為%@的格子里",((CollectionViewCell *)self.orignalCell).number, ((CollectionViewCell *)self.motherCell).number);
[self.dataArray removeObjectAtIndex:childIndexPath.row];
[self.collectionView deleteItemsAtIndexPaths:@[childIndexPath]];
}
} completion:^(BOOL finished) {
[self.bigMotherCell removeFromSuperview];
self.motherCell = nil;
[self stopAddTimer];
}];
}
}
最后就是結(jié)束長按的方法,他主要負(fù)責(zé)讓一切回歸初始
- (void)gestureEndOrCancle:(UILongPressGestureRecognizer *)longPressGesture {
self.collectionView.userInteractionEnabled = NO;
[self stopEdgeTimer];
if (self.motherCell) {
[self stopAddToCellWithData:YES];
[self removeTempMoveCell];
} else {
[UIView animateWithDuration:0.25 animations:^{
self.tempMoveCell.center = self.orignalCenter;
} completion:^(BOOL finished) {
[self removeTempMoveCell];
}];
}
}
這就是拖入-添加操作的所有邏輯,如果還不明白可以來這里下載Demo試試:
https://github.com/zyf25333/drop-addCollectionView