UIcollectionView的 “拖入—添加” 操作

在UIcollectionView中实现类似电脑资源管理器里的那种将文件拖入图标就可以完成添加操作的效果,如图:

QQ20181031-164939.gif

这个gif经过压缩,效果不太好。实际效果比图上顺滑很多。

首先我下载了 https://github.com/wazrx/XWDragCellCollectionView
这个控件作为基础,感谢作者!

XWDragCellCollectionView可以实现垂直/水平的滚动以及滑动排序,这个不是研究的重点,我就不重复造轮子,而是在它的基础上来改出我们想要的功能。

首先添加手势,用长按手势激活cell,来进行接下来的操作:

- (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;
    }
}

方法中分别对操作的开始、拖动和结束/取消做了处理,先看开始的方法:

- (void)gestureBegan:(UILongPressGestureRecognizer *)longPressGesture {
    
    self.originalIndexPath = [self.collectionView indexPathForItemAtPoint:[longPressGesture locationOfTouch:0 inView:longPressGesture.view]];
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:self.originalIndexPath];//拿到被长按的格子
    
    //下面这一片是对这个格子截图
    UIImage *snap;
    UIGraphicsBeginImageContextWithOptions(cell.bounds.size, 1.0f, 0);
    [cell.layer renderInContext:UIGraphicsGetCurrentContext()];
    snap = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //把截好的图片装进一个假的View里,之后用这个View随手指拖动而运动而隐藏原格子,给用户造成其实是格子被拖动的效果
    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];
    //开启边缘滚动定时器
    [self setEdgeTimer];
    //开启抖动,和原控件不同,我这个是只抖两下
    [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"];
}

接下来是重点的拖动时候调用的方法:

- (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方法则负责分辨具体的操作

#define MoveSpace 20    //在格子里的这么大范围内也算move操作的触发点,而不是add操作的触发点
- (void)handleCell {
    for (UICollectionViewCell *cell in [self.collectionView visibleCells]) {//遍历所有的可视cell
        if ([self.collectionView indexPathForCell:cell] == _originalIndexPath) {//如果是自己这个cell,那么跳过
            continue;
        }
        //计算所有表格中心和在移动的格子中心的距离
        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(@"进入格子内");
            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;
        }
    }
}

看代码到这里,你可能会发现setMoveTimer、setAddTimer 和 setEdgeTimer三个方法我并没有解释,这三个是计时器,用来延迟用户操作,提升手感。比如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;
    }
}

他规定了用户将格子移动到会触发move(排序)操作的位置时有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秒的缓冲,同时这两个计时器在生成时都会ban掉对方,防止操作混乱。

setEdgeTimer是负责拖动按钮移动到屏幕边缘触发滚动的。这个是原控件里就有的方法,不作讲解。

在移动计时器到时间后,就会触发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和结束add。结束add又分两种情况:完成add操作和取消add操作。完成add操作要对文件进行更改,取消add操作要将表格还原到操作之前的状态。首先看开始add:

- (void)addToCell {
//    NSLog(@"开始add操作");
    if (self.motherCell) {
//        NSLog(@"开始放大");
        //跟上面一样,对添加操作作为文件夹一方的格子(motherCell)进行截图
        UIImage *snap;
        UIGraphicsBeginImageContextWithOptions(self.motherCell.bounds.size, 1.0f, 0);
        [self.motherCell.layer renderInContext:UIGraphicsGetCurrentContext()];
        snap = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        //把截图装进一个假的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];
        
        //方法这个假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");
    }
}

结束add的两种情况我选择用一个bool值进行区分,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];
        }];
    }
}

最后就是结束长按的方法,他主要负责让一切回归初始

- (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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352