美文网首页ios学习iOS进阶iOS技术资料
可拖拽重排的CollectionView

可拖拽重排的CollectionView

作者: wazrx | 来源:发表于2016-01-05 16:50 被阅读15890次

    写在前面

    这段时间都在忙新项目的事儿,没有时间倒腾,这两天闲下来,想着一直没有细细的研究CollectionView,一般最多用来做点循环滚动,所以花时间深入学习了一些东西,这次实现了CollectionView的拖动重排的效果,先请看图:(吐槽:不知道为啥从xcode7开始,模拟器变得很卡很卡,所以截图的效果不好,大家可以在真机上测试,效果还是非常不错的)

    2月27日更新:

    修复了拖拽滚动时抖动的一个bug,新增编辑模式,进入编辑模式后不用长按触发手势,且在开启抖动的情况下会自动进入抖动模式,如图:


    test.gif

    图1:垂直滚动

    drag1.gif

    图2:水平滚动

    drag2.gif

    图3:配合瀑布流(我直接使用了上个项目的瀑布流模块做了集成实验)

    drag5.gif
    我将整个控件进行了封装,名字是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鼓励一下,谢谢!

    相关文章

      网友评论

      • 789342ad28c9:大神,如果Item的宽度不固定的话,想把item自动吸附在最左边怎么做?
      • 风火游龙:问题已经解决,谢谢楼主
      • 风火游龙:楼主你好,已经喜欢+关注了,请问一下你你这里的瀑布流拖拽是如何实现的,请不吝赐教574993357@qq.com
      • 731fbed25d23:楼主厉害呀 但是有个问题想问 多个分区的情况下 如果不允许第一个分区的数据之间互相移动 该怎么处理?不同分区之间点对点的交换的 应该怎么实现 @wazrx
      • 午马丶:黑名单功能。抱歉没仔细看
      • 午马丶:你好为什么最后一个cell不能交换啊
      • 陆大胖:刚在项目中增加了一个类似的功能。问题点在于indexPathForItemAtPoint:当手指停留在两个cell之间会返回nil导致“挤走”显得不灵敏。目前通过扩大cell的热区来优化。请问博主是否有好的意见呢?
      • 筱贰笔:大神给你提个BUG,我这两个section,如果把一个section里的item全拖到另一个section里,那么就无法向这个空的section里拖item了
      • 编号x71291:有个问题耶 你cell如果设置个cell.layer.cornerRadius = 8.0; 圆角 拖动的时候会有黑边存在 请问该怎么解决
        编号x71291:解决了 设置里面的
        tempMoveCell.layer.cornerRadius = 8.0;
        tempMoveCell.layer.masksToBounds = YES;就可以了
      • 7eg:卷纸小图标是亮点:clap:
      • 30d430c75985:楼主你好,项目里用了你封装好的collectionView基本达到想要的效果,但是我用的是swift编写,我想问下你在ViewController类里最后一个item禁止晃动,我按照swift仿照oc去写,一直写不出来,楼主能帮帮转一下嘛
      • 搜捕儿:为什么每个分区的最后一个都不可以拖动呢?
        搜捕儿:@____Hm 好的,谢谢啦
        30d430c75985:有一个方法viewViewController最后一个方法有写最后一个item不让拖动,如果想全部拖动注释就行了
      • aaa8f6d500e4:作者,首先给你点个赞,其次想问你一下,是出于什么考虑,让每组最后一个cell,不能交换
        马什么梅:我觉得还是挺有用的这个- - 我最后一个cell是一个+ 从相册选择图片= =
        aaa8f6d500e4:我看的是你github上的代码
      • NateLam:大神, 帮了我的大忙, 公司项目暂时还有适配ios8, 必须自己实现:+1:
      • NateLam:请问collectionview可以像tableview那样进入编辑模式, 实现多选删除吗
      • kakukeme:demo在xcode8下运行;拖拽的cell背景色消失了,
      • 程序员不务正业:遇到一个坑,不知道作者知不知道,集成,在控制器中写collectionVew是没问题的,但是把这个collectionView封装起来,放到一个view中,会出现无法排序的问题。
      • GQF_1102:崩溃在 [super removeObserver。。。。。] 那里 是什么问题呢

        两个代理方法都实现了 但是我的数据源是一个可变数组 你需要的和返回的都是 不可变数组 我给copy进去了 出来的时候 removeAll 然后 addFromArray了 这样就崩溃了 应该怎么操作不可变数组呢
      • GQF_1102:崩溃在 [super removeObserver。。。。。] 那里 是什么问题呢
      • 剧文轩:您好,在这里跟您提出一个BUG。就是在iphone7下截取当前cell的图形是一个白色的图形。截取失败了
      • 2bb5ebfd832c:我看你的原理似乎是把长按的响应时间设为0使触摸直接响应为长按,但是如果cell里面有其他需要交互的按钮比如有一个点击来删除的按钮,这样响应点击就变成长按了,就不会响应这个点击事件,这种情况怎么办呢?
        苏旋律:UIControlEventTouchDown用这个或者,还有另外一个防止冲突的属性。requireGestureRecognizerToFail
      • 正_文:@wazrx
        正_文:问题是,这句代码执行了一次,然后后面所有的cell都可以执行移动动画了哦。
        我试一下这句代码看看效果先,谢谢了。
        wazrx:@寒号bird 不好意思,最近太忙没看见,这个动画应该是moveItemAtIndexPath这个方法默认的交换动画,我没有自己去实现,如果想要修改这个动画,可以尝试重写layout中那几个动画的API 实现自己想要的动画效果
      • 正_文:xwp_updateDataSource,这个方法只是更新了数据源而已,cell动画怎么实现的呀
      • 正_文:楼主,能不能帮忙解释一下上面那个问题,谢谢了哦
      • 正_文:楼主,写的确实不错,代码我看懂了。
        就有一点不明白:拖拽的时候,比如从index=6拖到index=1的位置,index=1之后的cell依次往后滚动的动画在哪实现的,我一直没搞懂呢
        我也没有找到想过的代码
        Jixin:- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; 这个是移动cell的方法

        [CATransaction begin];
        [self moveItemAtIndexPath:_originalIndexPath toIndexPath:_moveIndexPath];
        [CATransaction setCompletionBlock:^{
        NSLog(@"动画完成");
        }];
        [CATransaction commit];
      • puppySweet:这近要做 两个cell 位置交换 而不是 让他自动重排 系统的这个重排顺序不符合要求啊 怎么办 交换位置 其他cell 不动
        731fbed25d23:只想点对点的交换 这个问题你这边解决了吗?
        苏旋律:我也是,只想点对点的交换,你后来解决了吗?
      • 吓坏了朕的龙体:抖得我眼花! :joy:
      • 来宝:牛逼
      • DrunkenMouse:从头到尾研究了很多遍,完完整整的仿写了出来。但是对于 边缘滚动定时器 实在不理解,因为帧动画中已经让动画次数为MAXFLOAT,那就会一直震动下去,甚至编辑模式的进入或退出也是通过帧动画,完全不需要注册监听content的监听值了,然后我试着把它们全都注释也不影响,所以,边缘滚动定时器有什么作用?楼主,还望指点一下,哪怕只是让我去查也能帮忙解答一下查些什么吗?
        DrunkenMouse:@wazrx 趁着午休又试了试,感觉已经完全明白楼主的思路了,谢谢楼主,辛苦了。不过向上滚动到最上端的判断确实好难。
        wazrx:@DrankMouse 边缘定时器不是用于抖动,是用于拖拽到边缘让collectionView能够滚动用的呢
      • Gxpzy:UIApplicationWillEnterForegroundNotification 你好,为什么要监听这个通知呢
        if (_editing) {
        [self xwp_shakeAllCell];
        }
      • efd0507a0501:写的不错,但是你这个scroll的滚动方向向上,似乎就不太行了
      • 低调_哲:问一下 我拖动完 怎么保存重新整理的数据呢
      • 菊上一枝梅:效果做的太棒了, 而且代码工整
      • 光头金项链的和尚:如果两组数据源 那个返回数组的代理方法怎么办
      • 文兴:我试了下貌似这个重排,不需要截图啊,直接改变cell的frame就好了,然后拖拽开始之后,获取当前点对应的cell,如果不是拖拽的cell,就调用moveItemAtIndexPath
      • edaacd05d89a:当section的item为0时,往该section里面拖动是不会成功的好像
        boom_9fcd:@小冯哥 往一个空section中添加新的item的问题,解决了吗?
        小冯哥:这个问题解决了吗?有思路没有,我也有同样的问题,而且,在向一个分区的最后位置拖拽item的时候,也不会成功
        edaacd05d89a:@xt612 有没有解决方法。。。。?
      • 8ea37f877096:你这也说自己是新手?卧槽,那我岂不是还没有入门!
        额,还有,那个,没有美女图片啊,差评!!
      • WTG:- (void)xwp_edgeScroll;
        [self xwp_setScrollDirection];
        这两个方法的作用是什么啊 原谅我菜 没看懂
        9e0d1d3135af:@WTG 滚动方向
      • C_JH:代码在Cell交换那里崩溃了 为什么
        wazrx:@给你两个雪糕 你运行 demo会崩溃嘛?
        C_JH:@wazrx 两个代理方法实现了,我设置的5个section 崩溃信息看不懂
        wazrx:@给你两个雪糕 求崩溃信息,两个代理方法有没实现,还有你是几个section?
      • 笨鸟后飞了:不简单,大牛
      • 花前月下:collectionview 在iOS8以后不是出了个新的属性,可以用来拖拽排序的吗?
        xsr:@wazrx 为什么晃来晃去:smiley:
        花前月下:@wazrx 哦。:stuck_out_tongue_winking_eye: 前几天偶然扫了眼苹果的文档。 :joy::joy:
        wazrx:@花前月下 是iOS9吧,我最后有说的

      本文标题:可拖拽重排的CollectionView

      本文链接:https://www.haomeiwen.com/subject/tnrzhttx.html