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

在上一篇文章中,为大家介绍了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

在此之前,我们先来简单的关注一下UICollectionViewFlowLayoutUICollectionViewLayout的关系: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
(注:在开发中,对于需要在外部的控制器中设置layout属性的,包括内边距、行间距、列间距、列数以及cell的初始大小等,可以为layout添加代理,使用代理方法返回)。

下面我们再看一个例子:
首先,我们先创建一个继承于UICollectionViewFlowLayout的layout子类,layout类中不做任何实现,然后在控制器中赋值给collectionView,控制器中关于collectionView和数据源的设置和上例一样,然后运行程序,查看效果:


效果图-2.PNG

我们发现,尽管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

总结:

基本上到这里,UICollectionVew的使用就结束了,如何能够将UICollectionVew使用的更好,关键就在于怎样更好的运用UICollectionViewLayout和UICollectionViewFlowLayout,这才是UICollectionVew的精髓所在。

有兴趣的同学,可以移步到GitHub中下载Demo,里面我为大家做了非常详细的注释。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容