布局万花筒:UIColletionview

UICollection是iOS6的时候引入的,它是同UITableview共享一套API设计,都是基于datasource和delegate,都继承自UIScrollView。但它又与UITableview有很大不同,它进行了进一步的抽象,将它的所有的子视图的位置、大小、transform委托给了一个单独的布局对象:UICollectionViewLayout。这是一个抽象类,我们可以继承它来实现任何想要的布局,系统也为我们提供了一个开箱即食的实现UICollectionViewFlowLayout。在我看来,没有任何布局是UICollenctionViewLayout不能实现的,如果有那就自定义一个。

UITableview只能提供竖直滑动的布局,而且默认情况下cell的宽度和tableView的宽度一致,而且cell的排列顺序也是挨次排列。UICollectionView则为我们提供了另一种可能:它能提供竖直滑动的布局也能提供竖屏滑动的布局,而且cell的位置、大小等完全由你自己决定。所以在我们用到水平滑动的布局时,不要忙着用UIScrollView去实现,可以先考虑UICollectionView能不能满足要求,还有一个好处是你不要自己考虑滑动视图cell的重用问题。

这篇文章会如何自定义UICollectionViewLayout来实现任意布局,默认你已经会使用系统提供的UICollectionViewFlowLayout来进行标准的Grid View布局了。

1、UICollectuonViewFlowLayout

系统为我们提供了一个自定义的布局实现:UICollectionViewFlowLayout,通过它我们可以实现Grid View类型的布局,也就是像一个一个格子挨次排列的布局,对于大多数的情况下,使用它就能满足我们的要求了。系统为我们提供了布局所用的参数,我们在使用的时候只需去确认这些参数就行:

NS_CLASS_AVAILABLE_IOS(6_0) @interface UICollectionViewFlowLayout : UICollectionViewLayout

@property (nonatomic) CGFloat minimumLineSpacing;

@property (nonatomic) CGFloat minimumInteritemSpacing;

@property (nonatomic) CGSize itemSize;

@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:

@property (nonatomic) UICollectionViewScrollDirection scrollDirection; // default is UICollectionViewScrollDirectionVertical

@property (nonatomic) CGSize headerReferenceSize;

@property (nonatomic) CGSize footerReferenceSize;

@property (nonatomic) UIEdgeInsets sectionInset;

// Set these properties to YES to get headers that pin to the top of the screen and footers that pin to the bottom while scrolling (similar to UITableView).

@property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);

@property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);

@end

如果说上面所说的GridView类型的布局不能满足我们的需求,这是就需要自定义一个Layout。

2、UICollectionViewLayout  VS   UICollectionViewFlowLayout

UICollectionViewFlowLayout继承自UICollectionViewLayout,我们可以直接使用它,我们只需要提供cell的大小,以及行间距、列间距,他就会自己计算出每个cell的位置以及UICollectionView的滑动范围contentSize。但它只能提供一个方向的滑动,也就是说我们自定义的类如果继承自UICollectionViewFlowLayout,则只能在一个方向上滑动的布局,要么水平方向要么竖直方向。反之,则需要继承自UICollectionViewLayout,UICollectionViewLayout是一个抽象类,不能直接使用。

3、自定义布局需要实现的方法

UICollectionViewLayout文档为我们列出了需要实现的方法:

以上列出的这六个方法不是都需要我们自己实现的,而是根据需要,选择其中的某些方法实现。

collectionViewContentSize

UICollection继承自UIScrollView,我们都知道UIScrollView的一个重要参数:contentSize,如果这个参数不对,那么你布局的内容就不能完全展示,而collectionViewContentSize就是为了得到这个参数,UICollection就像一个画板,而collectionViewContentSize则规定了画板的大小,如果是继承自UICollectionViewFlowLayout,而且每个section里面的cell大小是通过UICollectionViewFlowLayout的参数设定的,大小和位置也不在自定义的过程中随意更改,那么collectionViewContentSize是可以不自己重写的,系统会自己计算contentSize,如果是继承自UICollectionViewLayout,那就需要根据你自己的展示布局去提供合适的CGSize给collectionViewContentSize。

layoutAttributesForElementsInRect

这个方法的参数是UICollectionView当前的bounds,也就是视图当前的可见区域,返回值是一个包含对象为UICollectionViewLayoutAttributes的数组,UICollectionView的可见区域内包含cell、supplementary view、decoration view(这里统称cell,因为它们都是collectionView的一个子视图),它们的位置、大小等信息都由对应的UICollectionViewLayoutAttributes控制。默认情况下这个LayoutAttributes包含indexPath、frame、center、size、transform3D、alpha以及hidden属性。如果你还需要控制其他的属性,你可以自己自定义一个UICollectionViewLayoutAttributes的子类,加上任意你想要的属性。

布局属性对象(UICollectionViewLayoutAttributes)通过indexPath和cell关联起来,当collectionView展示cell时,会通过这些布局属性对象拿到布局信息。

返回原话题,layoutAttributesForElementsInRect方法的返回值是一个数组,这个数组里面是传递进来的可见区域内的cell所对应的UICollectionViewLayoutAttributes。

要拿到可见区域内的布局属性,通常的做法如下:

如果你是继承自UICollectionViewFlowLayout,并且设置好了itemSize、行间距、列间距等信息,那么你通过[super layoutAttributesForElementsInRect:rect]就能拿到可见区域内的布局属性,反之,则进入步奏2。

创建一个空数组,用于存放可见区域内的布局属性。

从UICollectionView的数据源中取出你需要展示的数据,然后根据你想要的布局计算出哪些indexPath在当前可见区域内,通过CGRectIntersectsRect函数可以判断两个CGRect是否有交集来确定。然后循环调用layoutAttributesForItemAtIndexPath:来确定每一个布局属性的frame等数据。同样,如果当前区域内有supplementary view或者decoration view,你也需要调用:layoutAttributesForSupplementaryViewOfKind:atIndexPath或者layoutAttributesForDecorationViewOfKind:atIndexPath,最后将这些布局属性添加到数组中返回。这里需要多说一点的是,有些布局属性在UICollectionViewLayout的prepareLayout就根据数据源全部计算了出来,比如瀑布流样式的布局,这个时候你就只需要返回布局属性的frame和当前可见区域有交集的对象就行。

layoutAttributesFor…IndexPath

这里用三个点,是因为有三个类似的方法:

layoutAttributesForItemAtIndexPath:

layoutAttributesForSupplementaryViewOfKind:atIndexPath:

layoutAttributesForDecorationViewOfKind:atIndexPath:

它们分别为cell、supplementaryView、decorationView返回布局属性,它们的实现不是必须的,它们只是为对应的IndexPath返回布局属性,如果你能通过其他方法拿到对应indexPath处的布局属性,那就没必要非要实现这几个方法。

以layoutAttributesForItemAtIndexPath:为例,你可以通过+[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]方法拿到一个布局属性对象,然后你可能需要访问你的数据源去算出该indexPath处的布局属性的frame等信息,然后赋值给它。

shouldInvalidateLayoutForBoundsChange

这个是用来告诉collectionView是否需要根据bounds的改变而重新计算布局属性,比如横竖屏的旋转。通常的写法如下:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

{

CGRect oldBounds = self.collectionView.bounds;

if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {

return YES;

}

return NO;

}

需要注意的是,当在滑动的过程中,需要对某些cell的布局进行更改,那么就需要在这个方法里面返回YES,告诉UICollectionView重新计算布局。因为一个cell的改变会引起整个UICollectionView布局的改变。

4、示例一:瀑布流实现

瀑布流的排列一般用于图片或者商品的展示,它的布局特点是等宽变高,cell的排列是找到最短的那一列,然后把cell放到那个位置,效果如下:


下面我们来看看具体的实现,这里的布局行间距和列间距都定位10,列数固定为3列,如上图所示。

系统提供给我们的UICollectionViewFlowLayout显然不能实现瀑布流的布局,因为它的默认实现是一行一列整齐对齐的,所以我们需要新建一个继承自UICollectionViewFlowLayout的类,然后来讲解一下这个类的实现。

prepareLayout

在讲解如何布局瀑布流之前需要先说明一下UICollectionViewFlowLayout的prepareLayout方法,他会在UICollectionView布局之前调用,调用[self.collectionView reloadData]和[self.collectionView.collectionViewLayout invalidateLayout]的时候prepareLayout也会进行调用,如果shouldInvalidateLayoutForBoundsChange返回YES,prepareLayout方法同样也会调用。所以这个函数是提前进行数据布局计算的绝佳地方。

在进行瀑布流布局的时候我们可以在prepareLayout里面根据数据源,计算出所有的布局属性并缓存起来:

- (void)prepareLayout {

[super prepareLayout];

//记录布局需要的contentSize的高度

self.contentHeight = 0;

//columnHeights数组会记录各列的当前布局高度

[self.columnHeights removeAllObjects];

//默认高度是sectionEdge.top

for (NSInteger i = 0; i < self.columnCount; i++) {

[self.columnHeights addObject:@(self.edgeInsets.top)];

}

//清除之前所以的布局属性数据

[self.attrsArray removeAllObjects];

//通过数据源拿到需要展示的cell数量

NSInteger count = [self.collectionView numberOfItemsInSection:0];

//开始创建每一个cell对应的布局属性

for (NSInteger index = 0; index < count; index++) {

//创建indexPath

NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];

//获取cell布局属性,在layoutAttributesForItemAtIndexPath里面计算具体的布局信息

UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];

[self.attrsArray addObject:attrs];

}

}

在layoutAttributesForItemAtIndexPath方法里面去根据参数indexPath拿到数据源里面对应位置的展示数据,根据等宽的前提,等比例的获得布局属性的高度,然后根据记录每列当前布局到的高度的数组columnHeights来找到当前布局最短的那一列,从而获取到布局属性的origin信息,这样在等宽的前提下就获取到了当前indexPath处的布局属性的frame信息。然后更新columnHeights里面的数据,并且让记录布局所需高度的变量contentHeight等于当前列高度数组里面的最大值。

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

//获取一个UICollectionViewLayoutAttributes对象

UICollectionViewLayoutAttributes *attrs = [super layoutAttributesForItemAtIndexPath:indexPath];

//列数是3,布局属性的宽度是固定的

CGFloat collectionViewW = self.collectionView.frame.size.width;

CGFloat width = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;

CGFloat height = 通过数据源以及宽度信息,获取对应位置的布局属性高度;

//找到数组内目前高度最小的那一列

NSInteger destColumn = 0;

CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];

for (NSInteger index = 1; index < self.columnCount; index++) {

CGFloat columnHeight = [self.columnHeights[index] doubleValue];

if (minColumnHeight > columnHeight) {

minColumnHeight = columnHeight;

destColumn = index;

break;

}

}

//根据列信息,计算出origin的x

CGFloat x = self.edgeInsets.left + destColumn * (width +self.columnMargin);

CGFloat y = minColumnHeight;

if (y != self.edgeInsets.top) {//不是第一行就加上行间距

y += self.rowMargin;

}

//得到布局属性的frame信息

attrs.frame = CGRectMake(x, y, width, height);

//更新最短那列的高度

self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));

//更新记录展示布局所需的高度

CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];

if (self.contentHeight < columnHeight) {

self.contentHeight = columnHeight;

}

return attrs;

}

滑动的过程在,cell会不断重用,系统会调用layoutAttributesForElementsInRect方法来获取当前可见区域内的布局属性,由于所有的布局属性都缓存了起来,则只需返回布局属性的frame和当前可见区域有交集的布局属性就行。

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

NSMutableArray *rArray = [NSMutableArray array];

for (UICollectionViewLayoutAttributes *cacheAttr in _attrsArray) {

if (CGRectIntersectsRect(cacheAttr.frame, rect)) {

[rArray addObject:cacheAttr];

}

}

return rArray;

}

最后由于我们自定义了每个cell的高度及布局,所以系统是不知道UICollectionView当前的contentSize的大小,所以我们需要在collectionViewContentSize方法里返回正确的size以确保所以cell都能正常滑动到可见区域里来。

-(CGSize)collectionViewContentSize {

return CGSizeMake(CGRectGetWidth(self.collectionView.frame), self.contentHeight + self.edgeInsets.bottom);

}

至此,瀑布流的布局就完成了,实现起来非常简单,最关键的地方就是计算布局属性的frame信息。

5、示例二:卡片吸顶布局

卡片吸顶布局的效果如下:

可以看到滑到顶部的cell本应该移出当前可见区域,但我们实现的效果是移到顶部后就悬停,并且可以被后来的cell覆盖。

实现的原理非常简单,cell的布局使用UICollectionViewFlowLayout就能实现,我们新建一个继承自UICollectionViewFlowLayout的子类,利用这个子类创建布局,可以利用UICollectionViewFlowLayout提供的参数来构建一个不吸顶展示的collectionView:

只需要提供给UICollectionViewFlowLayoutitemSize和minimumLineSpacing就行,行间距minimumLineSpacing设置为一个负数就能建立起互相叠加的效果。

要建立吸顶的效果,只需要在原来的布局基础上,判断布局属性frame小于布局顶部的y值,就将布局属性的frame的y值设置为顶部的y值就行,这样滑动到顶部的cell都会在顶部悬停下来。

@implementation CardCollectionViewFlowLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

{

//拿到当前可见区域内的布局属性

NSArray *oldItems = [super layoutAttributesForElementsInRect:rect];

//处理当前可见区域内的布局属性吸顶

[oldItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) {

[self recomputeCellAttributesFrame:attributes];

}];

return oldItems;

}

- (void)recomputeCellAttributesFrame:(UICollectionViewLayoutAttributes *)attributes

{

//获取悬停处的y值

CGFloat minY = CGRectGetMinY(self.collectionView.bounds) + self.collectionView.contentInset.top;

//拿到布局属性应该出现的位置

CGFloat finalY = MAX(minY, attributes.frame.origin.y);

CGPoint origin = attributes.frame.origin;

origin.y = finalY;

attributes.frame = (CGRect){origin, attributes.frame.size};

//根据IndexPath设置zIndex能确立顶部悬停的cell被后来的cell覆盖的层级关系

attributes.zIndex = attributes.indexPath.row;

}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

{

//由于cell在滑动过程中会不断修改cell的位置,所以需要不断重新计算所有布局属性的信息

return YES;

}

@end

在实现里面不需要-(CGSize)collectionViewContentSize方法的原因是,对于利用UICollectionViewFlowLayout来进行布局,而不是自定义的布局,系统会自动根据你设置的itemSize等信息计算出contentSize。

6、总结

通过上面的例子我们可以看到,UICollectionView相到于一个画板,而UICollectionViewLayout则可以帮我们组织画板的大小,以及画板内容的组织形态。在日常开发需求中,我们也需要重视UICollectionView,利用好它可以达到事半功倍的效果。

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

推荐阅读更多精彩内容