美文网首页ios奇门杂招移动端开发ios牛掰控件
UICollectionView详解(二)—— 自定义UICol

UICollectionView详解(二)—— 自定义UICol

作者: 沐泽sunshine | 来源:发表于2017-07-11 16:23 被阅读356次

    在上一篇文章中,为大家介绍了UICollectionView的基本使用方法,过程和内容都比较简单,有兴趣看的同学可以点击这里,本次将为大家介绍的是如何使用UICollectionLayout自定义复杂的布局。

    1、UICollectionViewLayout简介

    (1)基本方法

    在UICollectionViewLayout时,我们主要会重写它的以下几个方法

    - (void)prepareLayout;
    

    prepareLayout会在三个时机调用,第一次初始化layout的时候,刷新layout的时候以及方法- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds返回YES的时候

    - (CGSize)collectionViewContentSize;
    

    该方法返回collectionView的内容的大小

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

    该方法会返回rect范围内所有cell的布局属性UICollectionViewLayoutAttributes

    - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
    

    该方法返回对应indexPath下的cell的布局属性UICollectionViewLayoutAttributes

    • (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
    该方法返回在界面发生变化是是否要重新布局,返回YES则会重新布局
    
    • (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset;
    返回滑动后的collectonView的偏移量(滑动所停止的点),默认返回proposedContentOffset参数的值,在这里我们可以手动设置实际需要的偏移量
    #####(2)UICollectionViewLayout与UICollectionViewFlowLayout
    在此之前,我们先来简单的关注一下`UICollectionViewFlowLayout`和`UICollectionViewLayout`的关系:`UICollectionViewFlowLayout`是系统为我们封装的一个继承于`UICollectionViewLayout`的子类,系统已经写好了布局,所以如果我们在```- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; 
    ```方法中调用  ``` NSArray *attributesArr = [super layoutAttributesForElementsInRect:rect];```,可以得到系统为我们写好的布局,但是如果直接继承于UICollectionViewLayout,上述方法得不到任何布局,所以我们必须要重写`- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
    `方法,在方法中写好布局并调用,这样才能为cell布局。
    
    #####(3)UICollectionViewLayoutAttributes
    关于cell的布局,我们还需要着重看一个类:UICollectionViewLayoutAttributes,它就是我们上面一直所说的cell的布局类,cell所有的布局属性都是要写到该类中的,那它到底都有哪些属性呢:
    

    @property (nonatomic) CGRect frame;
    @property (nonatomic) CGPoint center;
    @property (nonatomic) CGSize size;
    @property (nonatomic) CATransform3D transform3D;
    @property (nonatomic) CGRect bounds );
    @property (nonatomic) CGAffineTransform transform ;
    @property (nonatomic) CGFloat alpha;
    @property (nonatomic) NSInteger zIndex; // default is 0
    @property (nonatomic, getter=isHidden) BOOL hidden;
    @property (nonatomic, strong) NSIndexPath *indexPath;

    改变了这些属性,并传递给layout,就可以改变cell的布局,所以归根到底,不管多复杂的布局,都是在改变这些属性。
    ###自定义UICollectionViewLayout具体实现
    下面,我们就在具体的实例中看一下,如果使用自定义layout:
    ####先为大家贴出代码:
    ######创建一个继承于UICollectionViewLayout的子类
    >.h
    
    

    import <UIKit/UIKit.h>

    @interface My_1Layout : UICollectionViewLayout
    @end

    >.m
    
    

    import "My_1Layout.h"

    @interface My_1Layout()
    {
    UIEdgeInsets _edgeInset;//内边距
    CGFloat _lineSpacing;//行间距
    CGFloat _columnsSpacing;//列间距
    NSInteger _columnsNum;//列数
    NSMutableArray *_columnsHeightArray;//用来存放所有列的高度
    CGFloat _maxHeight;//collectionContent最大高度
    }
    @property (nonatomic,strong) NSMutableArray *attributesArray;//用来存放所有的cell的布局

    @end

    @implementation My_1Layout

    • (instancetype)init{
      if ([super init]) {
      _edgeInset = UIEdgeInsetsMake(5, 10, 5, 10);
      _lineSpacing = 10;
      _columnsSpacing = 10;
      _columnsNum = 3;
      _maxHeight = _edgeInset.top;
      _columnsHeightArray = [NSMutableArray new];
      _columnsHeightArray = [NSMutableArray arrayWithCapacity:_columnsNum];
      }
      return self;
      }

    • (void)prepareLayout{
      /**
      切记,一定要先调用父类的prepareLayout
      */
      [super prepareLayout];

      [_columnsHeightArray removeAllObjects];
      for (int i = 0; i < _columnsNum ; i ++) {
      [_columnsHeightArray addObject:[NSNumber numberWithInteger:_edgeInset.top]];
      }

      [self.attributesArray removeAllObjects];
      /**
      调用layoutAttributesForItemAtIndexPath:方法,根据collectionView中cell的个数,使用for循环,创建对应个数的cell的attributes,并存放到_columnsHeightArray数组中(也可以将该过程放到layoutAttributesForElementsInRect:中去执行)
      /
      NSInteger cellNum = [self.collectionView numberOfItemsInSection:0];
      for (int i = 0; i < cellNum; i ++) {
      NSIndexPath
      indexPath=[NSIndexPath indexPathForItem:i inSection:0];
      UICollectionViewLayoutAttributes *attri = [self layoutAttributesForItemAtIndexPath:indexPath];
      [self.attributesArray addObject:attri];
      }
      }
      -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{

      return YES;
      }
      -(NSArray<UICollectionViewLayoutAttributes > )layoutAttributesForElementsInRect:(CGRect)rect{
      /

      直接返回之前存放好的所有cell的attributes(也可以将prepareLayout方法中for循环创建attributes的过程放到这里执行)
      /
      return self.attributesArray;
      }
      -(UICollectionViewLayoutAttributes
      )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath{
      UICollectionViewLayoutAttributes
      attributes=[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

      CGFloat cellW = (kScreenWidth-_edgeInset.left-_edgeInset.right-(_columnsNum-1)*_columnsSpacing)/_columnsNum;
      CGFloat cellH = indexPath.item%2==0?160:125;
      // 应该添加cell的列号
      NSInteger minHeightColumn = 0;
      // 应该添加cell的列的高度
      CGFloat minColumnHeight = [_columnsHeightArray[minHeightColumn] doubleValue];
      //循环 获取最小的列的高度和该列的列号
      for (int i = 1; i < _columnsHeightArray.count; i ++ ) {
      CGFloat tempH = [_columnsHeightArray[i] floatValue];
      if (minColumnHeight > tempH) {
      minColumnHeight = tempH;
      minHeightColumn = i;
      }
      }
      //为高度最小的列添加cell
      CGFloat cellY = [_columnsHeightArray[minHeightColumn] floatValue]+_lineSpacing;
      CGFloat cellX = _edgeInset.left + minHeightColumn * (cellW + _columnsSpacing);
      attributes.frame = CGRectMake(cellX, cellY, cellW, cellH);
      //保存最新的高度
      CGFloat newHeight = cellY+cellH;
      [_columnsHeightArray replaceObjectAtIndex:minHeightColumn withObject:[NSNumber numberWithInteger:newHeight]];
      //返回布局信息
      return attributes;
      }

    • (CGSize)collectionViewContentSize{
      //根据最高的列 设置collectionContentSize
      [_columnsHeightArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
      CGFloat maxHeight = [_columnsHeightArray[idx]floatValue];
      if (maxHeight > _maxHeight) {
      _maxHeight = maxHeight;
      }
      }];
      return CGSizeMake(kScreenWidth, _maxHeight);
      }

    • (NSMutableArray *)attributesArray{
      if (!_attributesArray) {
      _attributesArray = [NSMutableArray new];
      }
      return _attributesArray;
      }

    大概思路就是:首先初始化layout的各种属性和变量,在- (void)prepareLayout中循环调用-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:方法,为所有的cell添加布局,最后从-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:中将其返回即可。本例中,
    
    调用layout的方法也很简单,就正常创建layout,并赋给collectionView就可以了:
    

    My_1Layout layout = [[My_1Layout alloc]init];
    UICollectionView
    collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight-64) collectionViewLayout:layout];

    下面是效果图:
    ![效果图-1.gif](http:https://img.haomeiwen.com/i1914621/6c873fb7927b595e.gif?imageMogr2/auto-orient/strip)
    

    (注:在开发中,对于需要在外部的控制器中设置layout属性的,包括内边距、行间距、列间距、列数以及cell的初始大小等,可以为layout添加代理,使用代理方法返回)。

    
    下面我们再看一个例子:
    首先,我们先创建一个继承于UICollectionViewFlowLayout的layout子类,layout类中不做任何实现,然后在控制器中赋值给collectionView,控制器中关于collectionView和数据源的设置和上例一样,然后运行程序,查看效果:
    ![效果图-2.PNG](http:https://img.haomeiwen.com/i1914621/315258ac335f087d.PNG?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    我们发现,尽管layout没有做任何布局,但是collectionView任然可以显示,这就说明,UICollectionViewFlowLayout已经为我们做好了一个布局,就是我们现在看到的流水布局,所以,对于继承于UICollectionViewFlowLayout的 类,如果要改变cell的布局,只需要获取系统默认为cell写好的布局,然后再此基础上进行修改就可以了。那么怎样获取UICollectionViewFlowLayout为我们写好的布局呢,使用父类调用-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:。
    
    废话不多说,直接上代码:
    
    

    /**
    *prepareLayout会频繁调用,所以只做一些简单的初始化操作
    */

    • (void)prepareLayout{
      [super prepareLayout];
      // 水平滚动
      self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
      //设置cell大小(可放到外面的控制器中)
      self.itemSize = CGSizeMake(200, 200);
      //设置内边距
      CGFloat inset = (self.collectionView.frame.size.width - self.itemSize.width) * 0.5;
      self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);

    }
    /**
    *设置为YES,collectionView的显示范围发生变化时,就要刷新布局
    */
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{

    return YES;
    

    }
    /**
    *返回所有cell的布局属性
    */
    -(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    //获取UICollectionViweFlowLayout已经做好的布局
    NSArray attrbutesArray = [super layoutAttributesForElementsInRect:rect];
    //计算collectionView可视范围的中心点所对应的collectionView的x值
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
    //以每个cell中心点到centerX的距离为参考,对cell进行缩放
    for (UICollectionViewLayoutAttributes attributes in attrbutesArray) {
    //cell的中心点到centerX的距离
    CGFloat distance = ABS(attributes.center.x - centerX);
    //根据distance计算cell的缩放比例
    CGFloat scale = 1 - (distance / self.collectionView.frame.size.width);
    //设置缩放比例
    attributes.transform3D = CATransform3DMakeScale(scale, scale, scale);
    }
    // 返回调整之后的布局属性数组
    return attrbutesArray;
    }
    /

    • 在重新刷新布局时调用
      */
    • (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath {
      UICollectionViewLayoutAttributes attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
      return attr;
      }
      /
    • collectionView停止滑动时调用,可以手动设置collectionView的偏移量
    • proposedContentOffset collectionView原本的偏移量
      */
    • (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
      //计算出最终显示的矩形框
      CGRect endRect;
      endRect.origin.x = proposedContentOffset.x;
      endRect.origin.y = 0;
      endRect.size = CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height);

      //获得所有cell的布局属性
      NSArray *attributesArr = [super layoutAttributesForElementsInRect:endRect];

      //计算collectionView最中心点的x值
      CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
      //循环所有的布局属性,得到距离中心点最近的cell到中心点的距离
      CGFloat minDelta = MAXFLOAT;
      for (UICollectionViewLayoutAttributes *attr in attributesArr) {
      if(ABS(minDelta) > ABS(attr.center.x - centerX)) {
      minDelta = attr.center.x - centerX;
      }
      }
      //原来偏移量的x+距离中心点最近的cell到中心点的距离 将其设置为偏移量,该cell就会到中心点
      proposedContentOffset.x += minDelta;
      return proposedContentOffset;
      }

    效果图:
    
    ![效果图-3.gif](http:https://img.haomeiwen.com/i1914621/bc10257d76409d4d.gif?imageMogr2/auto-orient/strip)
    
    ###总结:
    基本上到这里,UICollectionVew的使用就结束了,如何能够将UICollectionVew使用的更好,关键就在于怎样更好的运用UICollectionViewLayout和UICollectionViewFlowLayout,这才是UICollectionVew的精髓所在。
    
    有兴趣的同学,可以移步到GitHub中下载[Demo](https://github.com/muzesunshine/UICollectionViewDemo),里面我为大家做了非常详细的注释。

    相关文章

      网友评论

      本文标题:UICollectionView详解(二)—— 自定义UICol

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