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

图片描述

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

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

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/d...


枢机主教
68 声望4 粉丝

iOS转型的Unity程序员