美文网首页iOS猛码计划ios开发整理实用轮子
确定你会使用UICollectionView?三个案列颠覆你的认

确定你会使用UICollectionView?三个案列颠覆你的认

作者: ZhengYaWei | 来源:发表于2017-03-22 13:37 被阅读1387次

    UICollectionView是针对IOS6 以后才能使用的控件,比起UITableView来说功能更强大,使用起来更方便,使用UICollectionView也可以完全取代UITableViewUICollectionView最重要的一点就是加载设置UICollectionViewFlowLayout。接下来就用三个demo来展示一下UICollectionView 的强大,对于iOS开发进阶还是有比较大的帮助的。

    Demo下载链接:https://github.com/ZhengYaWei1992/ZWAdvanceCollectionView

    第一个效果图是UICollectionViewCell的拖动,删除,之前开发一个项目中主页面的布局自定义排版使用过这个功能,所以给整理了下。

    第二个效果图是一个轻量级别的仿苹果的Cover Flow效果。主要实现是基于自定义布局的调整

    第三个效果图需要你仔细看一下。重点并不是联动,重点在于效果图中右侧UICollectionView组头是随着当前界面展示的section而浮动在界面上。效果看起来简单,实际上要想实现,设计的逻辑判断不是那么简单的。在UITableView上这个效果很好实现,只需要设置UITableViewStylePlain,想进一步了解UITableView中的组头浮动相关内容请参照我之前写过的一篇文章:http://www.jianshu.com/p/3b6d9a340e59

    看效果图啦,看效果图啦,看效果图啦,看效果图啦,看效果图啦。

    效果图一:cell的手势拖动移动 效果图二:苹果的coverFlow效果 效果图二:浮动组头

    接下来就一个一个看啦。

    1、UICollectionViewCell的移动。

    这个很简单,但是要重点说明一点。如果将长按手势添加到cell上手势不是很灵敏。解决的方法:将长按手势添加到self.collectionView上即可.

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        NSArray *arr = @[@"手机充值", @"亲民金融", @"就业招聘", @"乡间旅游",@"乡村医疗", @"违章查询", @"生活服务", @"乡村名宿",@"新农头条"];
        self.array = [NSMutableArray arrayWithArray:arr];
        _longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(lonePressMoving:)];
        [self.collectionView addGestureRecognizer:_longPress];
        [self.view addSubview:self.collectionView];
    }
    

    长按手势实现。

    - (void)lonePressMoving:(UILongPressGestureRecognizer *)longPress{
        switch (longPress.state) {
            case UIGestureRecognizerStateBegan:{//开始
                {
                    //获取长按的cell
                    NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
                    CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
                    //显示cell上的删除按钮
                    [cell.deleteButton setHidden:NO];
                    cell.deleteButton.tag = selectIndexPath.item;
                    //给当前cell上的删除按钮添加点击事件
                    [cell.deleteButton addTarget:self action:@selector(deleteButtonClick:) forControlEvents:UIControlEventTouchUpInside];
                    //设置collectionView开始移动
                    [_collectionView beginInteractiveMovementForItemAtIndexPath:selectIndexPath];
                }
                break;
            }
            case UIGestureRecognizerStateChanged:{//拖动中
                 [self.collectionView updateInteractiveMovementTargetPosition:[longPress locationInView:_longPress.view]];
                break;
            }
            case UIGestureRecognizerStateEnded:{//结束
                
                 [self.collectionView endInteractiveMovement];
                break;
            }
            default:
                [self.collectionView cancelInteractiveMovement];
                break;
        }
    }
    

    删除cell代码实现。

    - (void)deleteButtonClick:(UIButton *)deleteBtn{
        //cell的隐藏删除设置
        NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
        // 找到当前的cell
        CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
        cell.deleteButton.hidden = NO;
        //取出源item数据
        id objc = [self.array objectAtIndex:deleteBtn.tag];
        //从资源数组中移除该数据
        [self.array removeObject:objc];
        [self.collectionView reloadData];
    }
    

    要实现的代理方法以及特别注意的事项,具体代码中有说明。重点注意第四个代理方法的实现。

    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
        return self.array.count;
    }
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
        CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];
        cell.lable.text = self.array[indexPath.item];
        cell.deleteButton.hidden = YES;
        return cell;
    }
    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
        ViewController2 *vc = [[ViewController2 alloc]init];
        [self.navigationController pushViewController:vc animated:YES];
       
    }
    //交换collectionView必须要实现的代理方法
    - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(nonnull NSIndexPath *)sourceIndexPath toIndexPath:(nonnull NSIndexPath *)destinationIndexPath{
        NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
        // 找到当前的cell
        CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
        cell.deleteButton.hidden = YES;
        
        /*1.存在的问题,移动是二个一个移动的效果*/
        //  [collectionView moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
        /*2.存在的问题:只是交换而不是移动的效果*/
        //    [self.array exchangeObjectAtIndex:sourceIndexPath.item withObjectAtIndex:destinationIndexPath.item];
        /*3.完整的解决效果*/
        //取出源item数据
        id objc = [self.array objectAtIndex:sourceIndexPath.item];
        //从资源数组中移除该数据
        [self.array removeObject:objc];
        //将数据插入到资源数组中的目标位置上
        [self.array insertObject:objc atIndex:destinationIndexPath.item];
    
        [self.collectionView reloadData];
    }
    

    2、仿苹果Cover Flow效果。

    2.1、自定义流式布局

    这个效果的实现和第三个小何的实现核心在于自定义流式布局(UICollectionViewFlowLayout),涉及UICollectionView相关实心只要按照常规布局即可,只是在在设置布局的时候更改为自定义布局,这个自定义布局应该继承于UICollectionViewFlowLayout。如下代码,ZWCoverFlowLayout是继承与UICollectionViewFlowLayout的自定义类。

    ZWCoverFlowLayout *layout = [[ZWCoverFlowLayout alloc] init];
            //水平方向,元素之间的最小距离
            layout.minimumInteritemSpacing = 0;
            //行之间的最小距离
            layout.minimumLineSpacing = 20;
            layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
            //设置元素的大小
            UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 64, self.view.frame.size.width, 150) collectionViewLayout:layout];
    
    2.2、重写三个重要的系统方法。

    最重要的是看自定义流式布局类的代码实现。在这个自定义类中我们要重写系统的三个方法。三个方法的作用以及参数说明如下:
    设置布局属性。

    - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
    

    当bounds发生变化的时候是否需要重新布局

    - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
        //是否可以随着collectionView的滚动而变化
        return YES;
    }
    

    这个方法主要是为了停止滚动的时候,让一个cell显示到正中间
    返回值:当停止滚动的时候,人为停止的位置
    参一:当停止滚动的时候,自然情况下根据“惯性”停留的位置
    参二:每秒滚动多少个点

    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    
    2.3、具体实现代码。

    设置布局属性方法的实现。说明:每个cell唯一对应一个attribute对象,根据获取到的attribute对象我们可以直接设置每个cell的缩放布局等。

    - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
        //1、获取cell对应的attributes对象   每个cell唯一对应一个attribute对象
        NSArray *arrayAttrs = [super layoutAttributesForElementsInRect:rect];
        //计算整体的中心点的x值
        CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width * 0.5;
        //2、修改attributes对象
        for (UICollectionViewLayoutAttributes *attr in arrayAttrs) {
            //计算每个cell和中心点的具体
            CGFloat distance = ABS(attr.center.x - centerX);
            //距离越大,缩放比越小,距离越小,缩放比越大
            //缩放因子
            CGFloat factor = 0.003;
            CGFloat scale = 1 / (1 + distance * factor);
            attr.transform = CGAffineTransformMakeScale(scale, scale);
        }
        return arrayAttrs;
    }
    

    当bounds发生变化的时候是否需要重新布局

    - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
        //是否可以随着collectionView的滚动而变化
        return YES;
    }
    

    这个方法主要是为了停止滚动的时候,让一个特定cell显示到正中间,哪一个cell距离UICollectionView的中心比较近,就将哪一个cell显示到中间。

    //返回值:当停止滚动的时候,人为停止的位置
    //参一:当停止滚动的时候,自然情况下根据“惯性”停留的位置
    //参二:每秒滚动多少个点
    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
        //计算整体的中心点的值
        CGFloat centerX = proposedContentOffset.x + self.collectionView.bounds.size.width * 0.5;
        //这里不能使用contentOffset.x 因为手指一抬起来,contentOffset.x就不会再变化,按照惯性滚动的不会被计算到其中
        //CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width * 0.5;
        
        //计算可视区域
        CGFloat visibleX = proposedContentOffset.x;
        CGFloat visibleY = proposedContentOffset.y;
        CGFloat visibleW = self.collectionView.bounds.size.width;
        CGFloat visibleH = self.collectionView.bounds.size.height;
        //获取可视区域cell对应的attributes对象   每个cell唯一对应一个attribute对象
        NSArray *arrayAttrs = [super layoutAttributesForElementsInRect:CGRectMake(visibleX, visibleY, visibleW, visibleH)];
        
        //比较出最小的偏移
        int minIdx = 0;//假设最小的下标是0
        UICollectionViewLayoutAttributes *minAttr = arrayAttrs[minIdx];
        //循环比较出最小的
        for(int i = 1; i < arrayAttrs.count; i++){
            //计算两个距离
            //1、minAttr和中心点的距离
            CGFloat distance1 = ABS(minAttr.center.x - centerX);
            //2、计算出当前循环的attr对象和centerX的距离
            UICollectionViewLayoutAttributes *obj = arrayAttrs[i];
            CGFloat distance2 = obj.center.x - centerX;
            //3、比较
            if (distance2 < distance1) {
                minIdx = i;
                minAttr = obj;
            }
        }
        
        //计算出最小的偏移值
        CGFloat offsetX = minAttr.center.x - centerX;
        return CGPointMake(offsetX + proposedContentOffset.x, proposedContentOffset.y);
    }
    

    到此就OK了,如果还想扩充更多的,只要在样式上进行简单的调整即可。如翻转、拉升、动画效果等。

    3、UICollectionView的组头浮动。

    其实这个效果的实现,代码量不是很多,但是在逻辑处理上还是需要考虑很多的。所以下面的代码中,我会把每一步要做的事都用语言完全描述清楚。
    #######3、1 需要重写的系统方法
    这个效果的实现,和第二个Cover Flow效果一样,同样需要自定义流式布局。需要重写系统的两个方法。

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
    

    return YES:表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法

    - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound{
        return YES;
    }
    

    #######3、2 具体实现代码

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
        // UICollectionViewLayoutAttributes:我称它为collectionView中的item(包括cell和header、footer这些)的《结构信息》
        // 截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息),并转化成不可变数组
        NSMutableArray *superArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
        // 创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤)
        NSMutableIndexSet *noneHeaderSections = [NSMutableIndexSet indexSet];
        
        // 遍历superArray,得到一个当前屏幕中所有的section数组
        for (UICollectionViewLayoutAttributes *attributes in superArray){
            //如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤
            if (attributes.representedElementCategory == UICollectionElementCategoryCell){
                [noneHeaderSections addIndex:attributes.indexPath.section];
            }
        }
        
        // 遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
        // 正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法
        for (UICollectionViewLayoutAttributes *attributes in superArray){
            // 如果当前的元素是一个header,将header所在的section从数组中移除
            if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]){
                [noneHeaderSections removeIndex:attributes.indexPath.section];
            }
        }
        
        // 遍历当前屏幕中没有header的section数组
        [noneHeaderSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
            // 取到当前section中第一个item的indexPath
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
            // 获取当前section在正常情况下已经离开屏幕的header结构信息
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
            // 如果当前分区确实有因为离开屏幕而被系统回收的header
            if (attributes){
                // 将该header结构信息重新加入到superArray中去
                [superArray addObject:attributes];
            }
        }];
        // 遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示
        for (UICollectionViewLayoutAttributes *attributes in superArray){
            // 如果当前item是header
            if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]){
                // 得到当前header所在分区的cell的数量
                NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:attributes.indexPath.section];
                // 得到第一个item的indexPath
                NSIndexPath *firstItemIndexPath = [NSIndexPath indexPathForItem:0 inSection:attributes.indexPath.section];
                // 得到最后一个item的indexPath
                NSIndexPath *lastItemIndexPath = [NSIndexPath indexPathForItem:MAX(0, numberOfItemsInSection - 1) inSection:attributes.indexPath.section];
                // 得到第一个item和最后一个item的结构信息
                UICollectionViewLayoutAttributes *firstItemAttributes, *lastItemAttributes;
                if (numberOfItemsInSection > 0){
                    // cell有值,则获取第一个cell和最后一个cell的结构信息
                    firstItemAttributes = [self layoutAttributesForItemAtIndexPath:firstItemIndexPath];
                    lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItemIndexPath];
                }else{
                    // cell没值,就新建一个UICollectionViewLayoutAttributes
                    firstItemAttributes = [UICollectionViewLayoutAttributes new];
                    // 然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top
                    CGFloat y = CGRectGetMaxY(attributes.frame) + self.sectionInset.top;
                    firstItemAttributes.frame = CGRectMake(0, y, 0, 0);
                    // 因为只有一个cell,所以最后一个cell等于第一个cell
                    lastItemAttributes = firstItemAttributes;
                }
                
                // 获取当前header的frame
                CGRect rect = attributes.frame;
                // 当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置)
                CGFloat offset = self.collectionView.contentOffset.y + _navHeight;
                // 第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top
                CGFloat headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top;
                // 哪个大取哪个,保证header悬停
                // 针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理
                CGFloat maxY = MAX(offset, headerY);
                // 最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度
                // 当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值
                CGFloat headerMissingY = CGRectGetMaxY(lastItemAttributes.frame) + self.sectionInset.bottom - rect.size.height;
                // 给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小
                rect.origin.y = MIN(maxY, headerMissingY);
                // 给header的结构信息的frame重新赋值
                attributes.frame = rect;
                // 如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况
                // 通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大,这里随便填了个7
                attributes.zIndex = 7;
            }
        }
        // 转换回不可变数组,并返回
        return [superArray copy];
    }
    
    
    // return YES:表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
    - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound{
        return YES;
    }
    

    #######3、2 layoutAttributesForElementsInRect:方法代码实现思路总结。
    说明:superArray是截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息)
    1、遍历superArray,得到一个当前屏幕中所有的section数组
    2、遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
    3、遍历当前屏幕中没有header的section数组,如果当前分区确实有因为离开屏幕而被系统回收的header,将该header结构信息重新加入到superArray中去。
    4、遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示。这一步里面处理的逻辑是相对比较麻烦的,要获取到同个分组中最后一个和第一个item,然后根据区分上滚和下滚控制浮动组头的显示和隐藏。

    另外还有注意一个问题:
    如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况。所以这里要设置浮动组头的zIndex属性,保证其显示在最上方。

    到此,针对UICollectionView的浮动组头jiu'shi'xian

    相关文章

      网友评论

      本文标题:确定你会使用UICollectionView?三个案列颠覆你的认

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