iOS猛码计划ios开发整理实用轮子

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

2017-03-22  本文已影响1387人  ZhengYaWei

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

上一篇下一篇

猜你喜欢

热点阅读