UICollectionView自定义布局

本文转载于 iMetalk。

本文由 iMetalk 团队的成员 田向阳 完成,主要帮助读者使用UICollectionView进行自定义布局

前言

上一期,由我们的污大神给大家讲解了如何简单的使用tableview,想必大家一定会很想亲自上手去练练吧,但是这一期我就不讲tableview了,不过看了标题就知道这一期我要讲什么了,没错,就是UICollectionView,不过可能让大家失望的是,我并不会教大家怎么简单的使用CollectionView,而是会给大家安利一些关于UICollectionView的一些常规的知识点,帮助大家在以后项目中如何轻松的实现某些效果和样式!

俗话说的好:没有什么界面是一个 UITableView 解决不了的,如果不行,那就俩个! 污大神这句话说的一点都不错,在这个tableview横行的年代,你说你不会用tableview,那么真的只有 呵呵 了~~。

不过UICollectionView相对于UITableView可以说是青出于蓝而胜于蓝(话也不能这么说,毕竟他俩都是继承于UIScrollView),它和UITableView很相似,但它要更加强大,并不是因为tableview不够用,而是因为在UICollectionView中Apple给我们打开了一扇通向无限可能的大门:UICollectionViewFlowLayout.下面我就慢慢给大家讲解这个大门,如何去打开。

UICollectionView 基础:

这个部分不做细讲,可直接查看系统的API 查看相关的方法,大部分用法类似tableview

  • UICollectionViewDataSource 数据源协议
  • UICollectionViewDelegate 委托协议
  • UICollectionViewFlowLayout 视图布局对象(这个是流视图布局,继承于UICollectionViewLayout)基本上这个布局可以说是自动排版,也就是一行排满,就自动换行。如果我们要自定义样式的话,一般继承UICollectionViewFlowLayout就行了。
  • UICollectionViewDelegateFlowLayout 这个是FlowLAyout的代理协议 可以通过这些个协议来调整 cell大大小 和 layout的一些属性 (minimumInteritemSpacing,minimumLineSpacing,sectionInset等)

UICollectionView 创建:

// 初始化
self.collectionView = ({
    CollectionViewFlowLayout *layout = [[CollectionViewFlowLayout alloc] init];
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 64, CGRectGetWidth(self.view.frame), CGRectGetWidth(self.view.frame)/2.0) collectionViewLayout:layout];
    [self.view addSubview:collectionView];
    collectionView.backgroundColor = [UIColor whiteColor];//我为什么要设置为白色呢,以为没有数据的时候就是一片漆黑!!!
    collectionView.delegate = self;
    collectionView.dataSource = self;
    collectionView.showsHorizontalScrollIndicator = NO;
//注册Cell
    [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
    collectionView;
    });

//必须实现的两个数据源协议方法 
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0  blue:arc4random()%255/255.0  alpha:1];
    return cell;

}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 2;
}

// 还有一部分相关的代理方法 就不一一列举了  如  和UITableView一样 UICollectionView也可设置段头段尾
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{}

//控制单元格的大小 就用到了  UICollectionViewDelegateFlowLayout
#pragma mark ---- UICollectionViewDelegateFlowLayout

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(100.f,100.f);
}

//collectionView 上下左右的偏移  类似 scroll的 contentInset
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
    return UIEdgeInsetsMake(5, 5, 5, 5);
}
//区头  区尾 的大小 高度 
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
    return CGSizeMake(100.f,100.f);
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
{
    return (CGSize){ScreenWidth,22};
}

代码中的 CollectionViewFlowLayout 就是我自己定义的layout继承于UICollectionViewFlowLayout

UICollectionView自定义布局:

关于布局,而且用UICollectionView去布局,最常见的莫过于九宫格了吧,相信绝大多数的app里面多多少少都会用到这样的布局,在没有collectionView的年代,布局九宫格大家是不是跟我一样要用个for循环,然后计算出相应的坐标去排布呢?另外还有在两年前比较流行的 瀑布流布局刚开始怎么实现呢,用scrollView?亦或tableview 但是这两个都太麻烦,不过有了collectionView实现这样的效果就省事多了。

要自定义UICollectionView布局,就要创建自己的layout继承于UICollectionViewFlowLayout,然后重写父类的几个方法就可以达到我们自定义布局的需求。下来我们来看看UICollectionViewFlowLayout类里一些比较重要的方法

  • - (void)prepareLayout;
    为layout显示做准备工作,你可以在该方法里设置一些属性。另外说一下里面可能会遇到的坑,那就是计算单元格大小的时候,比如你要做一个九宫格,那么你计算的每一个宫格的大小 就应该把 宫格的间距 和左右的边距都给算进去 然后再根据具体需求去计算大小 否则计算出来的大小会有问题 一句话 细心为妙!!

  • - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
    这个方法返回的是在collectionView的可见范围所有item对应的UICollectionViewLayoutAttributes对象的数组。collectionView的每个item都对应一个专门的UICollectionViewLayoutAttributes类型的对象来表示该item的一些属性,他不是一个视图,但包含了视图所有的属性,比如,frame transform 等

  • - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{return YES;}
    当前layout的布局发生变动时,是否重写加载该layout。默认返回的是NO,若返回YES,则重新执行上面的两个方法。讲一下最初遇到的坑,第一次自己写layout的时候,由于没有写这个方法,(没写的话是默认为NO,不刷新layout,然后自己在 layoutAttributesForElementsInRect 方法里更改了相应的Attributes属性 但是得到的效果和自己想要的效果差距太大,太大。后来还是在度娘的帮助下把这个问题给解决,重写了方法并设返回值为 YES!

  • - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    这个方法 是返回最终collectionView的偏移量,也就是collectionView停止滚动时候的偏移量,通过这个方法可以控制你最终想要让collectionView停止的位置

项目实战

最终的效果图


collectionViewLayout.gif

前面已经讲过了关于自定义layout的几个重要的方法,那么下面就开始进入实战吧,下面我就先写一个我们项目中的一个小的例子,至于其他的效果,就要看你们自己的脑洞有多大了,废多看码~

    - (void)prepareLayout
    {
    [super prepareLayout];
    //设置滚动方向为横向滚动
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    //设置单元格的大小
    self.itemSize = CGSizeMake(itemWidth, itemWidth);
    //设置item间距
    self.minimumInteritemSpacing = 5;
    self.minimumLineSpacing = 5;
    //设置左右间距 在collectionView初始位置 和 最后位置保证在也停留在正中间
    self.sectionInset = UIEdgeInsetsMake(0, (self.collectionView.frame.size.width - itemWidth)*SCALE, 0, (self.collectionView.frame.size.width - itemWidth)*SCALE);
    }

    //开启实时刷新布局
    - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    {
        return YES;
    }

    // 调整当前layout的样式
    - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    //拿到当前视图内的layout组成的数组
    NSArray *temp  =  [super  layoutAttributesForElementsInRect:rect];
    NSMutableArray *attAtrray = [NSMutableArray array];
    //计算出相对于collectionView中心点 collectionView的实际偏移量
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width*0.5;
    for (int i = 0; i < temp.count; i ++) {
    UICollectionViewLayoutAttributes *att = [temp[i] copy]; //做copy操作 消除一下警告
    //计算据两边的cell 距离中间的偏移 取绝对值
    CGFloat offset = ABS(att.center.x - centerX);
    // 计算出scale 并调整 layout的transform属性
    CGFloat scale = 1 - offset / self.collectionView.frame.size.width*0.5;
    att.transform = CGAffineTransformMakeScale(scale, scale);

    [attAtrray addObject:att];
    }

    return  attAtrray;
    }


    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    {
    CGRect  rect;
    rect.origin = proposedContentOffset;
    rect.size = self.collectionView.frame.size;
    NSArray *tempArray  = [super  layoutAttributesForElementsInRect:rect];
    CGFloat  gap = 1000;
    CGFloat  a = 0;

    for (int i = 0; i < tempArray.count; i++) {
    //判断和中心的距离,得到最小的那个
    UICollectionViewLayoutAttributes *att = tempArray[i];
    gap = ABS(att.center.x - proposedContentOffset.x - self.collectionView.frame.size.width * SCALE);
    if (gap < self.collectionView.frame.size.width *SCALE *SCALE) {
    //同样是计算出左右间距的偏移
    a = att.center.x - proposedContentOffset.x - self.collectionView.frame.size.width * SCALE;
    break;
    }
    }
    //调整x的偏移
    CGPoint  point = CGPointMake(proposedContentOffset.x + a , proposedContentOffset.y);
    return point;
    }

大家在实践过程中,可以去尝试着把 shouldInvalidateLayoutForBoundsChange 这个方法给注释了,可以看一下注释后的效果。另外,关于我在注视的地方提到的警告 可能重写layout的同学都会遇到 下面我把错误代码粘上来 。


2017-03-09 12:44:11.483 CollectionViewLayout[11131:92643] Logging only once for UICollectionViewFlowLayout cache mismatched frame
2017-03-09 12:44:11.484 CollectionViewLayout[11131:92643] UICollectionViewFlowLayout has cached frame mismatch for index path  {length = 2, path = 0 - 0} - cached value: {{104.02791666666667, 10.02791666666667}, {167.44416666666666, 167.44416666666666}}; expected value: {{104, 10}, {167.5, 167.5}}
2017-03-09 12:44:11.484 CollectionViewLayout[11131:92643] This is likely occurring because the flow layout subclass CollectionViewFlowLayout is modifying attributes returned by UICollectionViewFlowLayout without copying them

消除警告的方法就是 在对 UICollectionViewLayoutAttributes 对象更改属性的时候要做一下copy操作,除此之外 要用另一个数组重新组装一下 然后return 。另外关于 copy 和 mutableCopy 有兴趣的可以去研究研究,我就不在此多说了。

总结

在自定义collectionViewLayout的时候,难免会踩到坑,但是坑归坑,关键我们要心细,胆大,多去尝试,这样才能解决问题。关于collectionView自定义layout的简单介绍,大概就这么多了,可能也有一部分没有讲解出来,但是万变不离其宗,只要你理解了里面的原理和你自己想要的效果,剩下的事情就是代码的事情了~

demo 地址

如果您想第一时间看到我们的文章,欢迎关注公众号。


微信公众号

===== 我是有底线的 ======
喜欢我的文章,欢迎关注我的新浪微博 Lefe_x,我会不定期的分享一些开发技巧

你可能感兴趣的:(UICollectionView自定义布局)