欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

UICollectionViewLayout的使用

程序员文章站 2023-04-06 22:35:25
uicollectionviewlayout 这是博主的wwdc2012笔记系列中的一篇,完整的笔记列表可以参看at.com/2012/06/%e5%bc%80%e5%8f%91%e8%80%85%...

uicollectionviewlayout

这是博主的wwdc2012笔记系列中的一篇,完整的笔记列表可以参看at.com/2012/06/%e5%bc%80%e5%8f%91%e8%80%85%e6%89%80%e9%9c%80%e8%a6%81%e7%9f%a5%e9%81%93%e7%9a%84ios6-sdk%e6%96%b0%e7%89%b9%e6%80%a7/" target="_blank">这里。如果您是首次来到本站,也许您会有兴趣通过rss,或者通过页面左侧的邮件订阅的方式订阅本站。

在上一篇uicollectionview的入门介绍中,大概地对ios6新加入的强大的uicollectionview进行了一些说明。在这篇博文中,将结合wwdc2012 session219:advanced collection view的内容,对collection view进行一个深入的使用探讨,并给出一个自定义的demo。

uicollectionview的结构回顾

首先回顾一下collection view的构成,我们能看到的有三个部分:

  • cells
  • supplementary views 追加视图 (类似header或者footer)
  • decoration views 装饰视图 (用作背景展示)

而在表面下,由两个方面对uicollectionview进行支持。其中之一和tableview一样,即提供数据的uicollectionviewdatasource以及处理用户交互的uicollectionviewdelegate。另一方面,对于cell的样式和组织方式,由于collectionview比tableview要复杂得多,因此没有按照类似于tableview的style的方式来定义,而是专门使用了一个类来对collectionview的布局和行为进行描述,这就是uicollectionviewlayout。

这次的笔记将把重点放在uicollectionviewlayout上,因为这不仅是collectionview和tableview的最重要求的区别,也是整个uicollectionview的精髓所在。

如果对uicollectionview的基本构成要素和使用方法还不清楚的话,可以移步到我之前的一篇笔记:session笔记——205 introducing collection views中进行一些了解。

 

uicollectionviewlayoutattributes

uicollectionviewlayoutattributes是一个非常重要的类,先来看看property列表:

  • @property (nonatomic) cgrect frame
  • @property (nonatomic) cgpoint center
  • @property (nonatomic) cgsize size
  • @property (nonatomic) catransform3d transform3d
  • @property (nonatomic) cgfloat alpha
  • @property (nonatomic) nsinteger zindex
  • @property (nonatomic, getter=ishidden) bool hidden

可以看到,uicollectionviewlayoutattributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和datasource的行为十分类似,当uicollectionview在获取布局时将针对每一个indexpath的部件(包括cell,追加视图和装饰视图),向其上的uicollectionviewlayout实例询问该部件的布局信息(在这个层面上说的话,实现一个uicollectionviewlayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以uicollectionviewlayoutattributes的实例的方式给出。

 

自定义的uicollectionviewlayout

uicollectionviewlayout的功能为向uicollectionview提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承uicollectionviewlayout类,然后重载下列方法:

-(cgsize)collectionviewcontentsize
  • 返回collectionview的内容的尺寸
-(nsarray *)layoutattributesforelementsinrect:(cgrect)rect
  • 返回rect中的所有的元素的布局属性
  • 返回的是包含uicollectionviewlayoutattributes的nsarray
uicollectionviewlayoutattributes可以是cell,追加视图或装饰视图的信息,通过不同的uicollectionviewlayoutattributes初始化方法可以得到不同类型的uicollectionviewlayoutattributes:
  • layoutattributesforcellwithindexpath:
  • layoutattributesforsupplementaryviewofkind:withindexpath:
  • layoutattributesfordecorationviewofkind:withindexpath:
-(uicollectionviewlayoutattributes)layoutattributesforitematindexpath:(nsindexpath)indexpath
  • 返回对应于indexpath的位置的cell的布局属性
-(uicollectionviewlayoutattributes)layoutattributesforsupplementaryviewofkind:(nsstring)kind atindexpath:(nsindexpath *)indexpath
  • 返回对应于indexpath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(uicollectionviewlayoutattributes * )layoutattributesfordecorationviewofkind:(nsstring)decorationviewkind atindexpath:(nsindexpath)indexpath
  • 返回对应于indexpath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(bool)shouldinvalidatelayoutforboundschange:(cgrect)newbounds
  • 当边界发生改变时,是否应该刷新布局。如果yes则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。

另外需要了解的是,在初始化一个uicollectionviewlayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。

首先,-(void)preparelayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

之后,-(cgsize) collectionviewcontentsize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionview的本质是一个scrollview,因此需要这个尺寸来配置滚动行为。

接下来-(nsarray *)layoutattributesforelementsinrect:(cgrect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的uicollectionviewlayoutattributes来决定。

另外,在需要更新layout时,需要给当前layout发送 -invalidatelayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和uiview的setneedslayout方法十分类似。在-invalidatelayout后的下一个collectionview的刷新loop中,又会从preparelayout开始,依次再调用-collectionviewcontentsize和-layoutattributesforelementsinrect来生成更新后的布局。

 

demo

说了那么多,其实还是demo最能解决问题。apple官方给了一个flow layout和一个circle layout的例子,都很经典,需要的同学可以从。

linelayout——对于个别uicollectionviewlayoutattributes的调整

先看linelayout,它继承了uicollectionviewflowlayout这个apple提供的基本的布局。它主要实现了单行布局,自动对齐到网格以及当前网格cell放大三个特性。如图:

 

先看linelayout的init方法:

 -(id)init

{

self = [super init];

if (self) {

self.itemsize = cgsizemake(item_size, item_size);

self.scrolldirection = uicollectionviewscrolldirectionhorizontal;

self.sectioninset = uiedgeinsetsmake(200, 0.0, 200, 0.0);

self.minimumlinespacing = 50.0;

}

return self;

}

self.sectioninset = uiedgeinsetsmake(200, 0.0, 200, 0.0); 确定了缩进,此处为上方和下方各缩进200个point。由于cell的size已经定义了为200x200,因此屏幕上在缩进后就只有一排item的空间了。

self.minimumlinespacing = 50.0; 这个定义了每个item在水平方向上的最小间距。

uicollectionviewflowlayout是apple为我们准备的开袋即食的现成布局,因此之前提到的几个必须重载的方法中需要我们操心的很少,即使完全不重载它们,现在也可以得到一个不错的线状一行的gridview了。而我们的linelayout通过重载父类方法后,可以实现一些新特性,比如这里的动对齐到网格以及当前网格cell放大。

自动对齐到网格

- (cgpoint)targetcontentoffsetforproposedcontentoffset: (cgpoint)proposedcontentoffset withscrollingvelocity:(cgpoint)velocity

{

//proposedcontentoffset是没有对齐到网格时本来应该停下的位置

cgfloat offsetadjustment = maxfloat;

cgfloat horizontalcenter = proposedcontentoffset.x + (cgrectgetwidth(self.collectionview.bounds) / 2.0);

cgrect targetrect = cgrectmake(proposedcontentoffset.x, 0.0, self.collectionview.bounds.size.width, self.collectionview.bounds.size.height);

nsarray* array = [super layoutattributesforelementsinrect:targetrect];

 

//对当前屏幕中的uicollectionviewlayoutattributes逐个与屏幕中心进行比较,找出最接近中心的一个

for (uicollectionviewlayoutattributes* layoutattributes in array) {

cgfloat itemhorizontalcenter = layoutattributes.center.x;

if (abs(itemhorizontalcenter - horizontalcenter) < abs(offsetadjustment)) {

offsetadjustment = itemhorizontalcenter - horizontalcenter;

}

}

return cgpointmake(proposedcontentoffset.x + offsetadjustment, proposedcontentoffset.y);

}

当前item放大

-(nsarray *)layoutattributesforelementsinrect:(cgrect)rect

{

nsarray *array = [super layoutattributesforelementsinrect:rect];

cgrect visiblerect;

visiblerect.origin = self.collectionview.contentoffset;

visiblerect.size = self.collectionview.bounds.size;

 

for (uicollectionviewlayoutattributes* attributes in array) {

if (cgrectintersectsrect(attributes.frame, rect)) {

cgfloat distance = cgrectgetmidx(visiblerect) - attributes.center.x;

cgfloat normalizeddistance = distance / active_distance;

if (abs(distance) < active_distance) {

cgfloat zoom = 1 + zoom_factor*(1 - abs(normalizeddistance));

attributes.transform3d = catransform3dmakescale(zoom, zoom, 1.0);

attributes.zindex = 1;

}

}

}

return array;

}

对于个别uicollectionviewlayoutattributes进行调整,以达到满足设计需求是uicollectionview使用中的一种思路。在根据位置提供不同layout属性的时候,需要记得让-shouldinvalidatelayoutforboundschange:返回yes,这样当边界改变的时候,-invalidatelayout会自动被发送,才能让layout得到刷新。

circlelayout——完全自定义的layout,添加删除item,以及手势识别

circlelayout的例子稍微复杂一些,cell分布在圆周上,点击cell的话会将其从collectionview中移出,点击空白处会加入一个cell,加入和移出都有动画效果。

这放在以前的话估计够写一阵子了,而得益于uicollectionview,基本只需要100来行代码就可以搞定这一切,非常cheap。通过circlelayout的实现,可以完整地看到自定义的layout的编写流程,非常具有学习和借鉴的意义。

 

circlelayout

首先,布局准备中定义了一些之后计算所需要用到的参数。

-(void)preparelayout

{ //init相似,必须call superpreparelayout以保证初始化正确

[super preparelayout];

 

cgsize size = self.collectionview.frame.size;

_cellcount = [[self collectionview] numberofitemsinsection:0];

_center = cgpointmake(size.width / 2.0, size.height / 2.0);

_radius = min(size.width, size.height) / 2.5;

}

其实对于一个size不变的collectionview来说,除了_cellcount之外的中心和半径的定义也可以扔到init里去做,但是显然在preparelayout里做的话具有更大的灵活性。因为每次重新给出layout时都会调用preparelayout,这样在以后如果有collectionview大小变化的需求时也可以自动适应变化。

然后,按照uicollectionviewlayout子类的要求,重载了所需要的方法:

 //整个collectionview的内容大小就是collectionview的大小(没有滚动)

-(cgsize)collectionviewcontentsize

{

return [self collectionview].frame.size;

}

 

//通过所在的indexpath确定位置。

- (uicollectionviewlayoutattributes *)layoutattributesforitematindexpath:(nsindexpath *)path

{

uicollectionviewlayoutattributes* attributes = [uicollectionviewlayoutattributes layoutattributesforcellwithindexpath:path]; //生成空白的attributes对象,其中只记录了类型是cell以及对应的位置是indexpath

//配置attributes到圆周上

attributes.size = cgsizemake(item_size, item_size);

attributes.center = cgpointmake(_center.x + _radius * cosf(2 * path.item * m_pi / _cellcount), _center.y + _radius * sinf(2 * path.item * m_pi / _cellcount));

return attributes;

}

 

//用来在一开始给出一套uicollectionviewlayoutattributes

-(nsarray*)layoutattributesforelementsinrect:(cgrect)rect

{

nsmutablearray* attributes = [nsmutablearray array];

for (nsinteger i=0 ; i < self.cellcount; i++) {

//这里利用了-layoutattributesforitematindexpath:来获取attributes

nsindexpath* indexpath = [nsindexpath indexpathforitem:i insection:0];

[attributes addobject:[self layoutattributesforitematindexpath:indexpath]];

}

return attributes;

}

现在已经得到了一个circle layout。为了实现cell的添加和删除,需要为collectionview加上手势识别,这个很简单,在viewcontroller中:

uitapgesturerecognizer* taprecognizer = [[uitapgesturerecognizer alloc] initwithtarget:self action:@selector(handletapgesture:)];

[self.collectionview addgesturerecognizer:taprecognizer];

对应的处理方法handletapgesture:为

 - (void)handletapgesture:(uitapgesturerecognizer *)sender {

if (sender.state == uigesturerecognizerstateended) {

cgpoint initialpinchpoint = [sender locationinview:self.collectionview];

nsindexpath* tappedcellpath = [self.collectionview indexpathforitematpoint:initialpinchpoint]; //获取点击处的cellindexpath

if (tappedcellpath!=nil) { //点击处没有cell

self.cellcount = self.cellcount - 1;

[self.collectionview performbatchupdates:^{

[self.collectionview deleteitemsatindexpaths:[nsarray arraywithobject:tappedcellpath]];

} completion:nil];

} else {

self.cellcount = self.cellcount + 1;

[self.collectionview performbatchupdates:^{

[self.collectionview insertitemsatindexpaths:[nsarray arraywithobject:[nsindexpath indexpathforitem:0 insection:0]]];

} completion:nil];

}

}

}

performbatchupdates:completion: 再次展示了block的强大的一面..这个方法可以用来对collectionview中的元素进行批量的插入,删除,移动等操作,同时将触发collectionview所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:

  • initiallayoutattributesforappearingitematindexpath:
  • initiallayoutattributesforappearingdecorationelementofkind:atindexpath:
  • finallayoutattributesfordisappearingitematindexpath:
  • finallayoutattributesfordisappearingdecorationelementofkind:atindexpath:

更正:正式版中api发生了变化(而且不止一次变化 initiallayoutattributesforinserteditematindexpath:在正式版中已经被废除。现在在insert或者delete之前,prepareforcollectionviewupdates:会被调用,可以使用这个方法来完成添加/删除的布局。关于更多这方面的内容以及新的示例demo,可以参看(需要*)。新的示例demo在github上也有,链接

在circlelayout中,实现了cell的动画。

/插入前,cell在圆心位置,全透明

- (uicollectionviewlayoutattributes *)initiallayoutattributesforinserteditematindexpath:(nsindexpath *)itemindexpath

{

uicollectionviewlayoutattributes* attributes = [self layoutattributesforitematindexpath:itemindexpath];

attributes.alpha = 0.0;

attributes.center = cgpointmake(_center.x, _center.y);

return attributes;

}

 

//删除时,cell在圆心位置,全透明,且只有原来的1/10

- (uicollectionviewlayoutattributes *)finallayoutattributesfordeleteditematindexpath:(nsindexpath *)itemindexpath

{

uicollectionviewlayoutattributes* attributes = [self layoutattributesforitematindexpath:itemindexpath];

attributes.alpha = 0.0;

attributes.center = cgpointmake(_center.x, _center.y);

attributes.transform3d = catransform3dmakescale(0.1, 0.1, 1.0);

return attributes;

}

在插入或删除时,将分别以插入前和删除后的attributes和普通状态下的attributes为基准,进行uiview的动画过渡。而这一切并没有很多代码要写,几乎是free的,感谢苹果…

 

布局之间的切换

有时候可能需要不同的布局,apple也提供了方便的布局间切换的方法。直接更改collectionview的collectionviewlayout属性可以立即切换布局。而如果通过setcollectionviewlayout:animated:,则可以在切换布局的同时,使用动画来过渡。对于每一个cell,都将有对应的uiview动画进行对应,又是一个接近free的特性。

对于我自己来说,uicollectionview可能是我转向ios 6 sdk的最具有吸引力的特性之一,因为uikit团队的努力和coreanimation的成熟,使得创建一个漂亮优雅的ui变的越来越简单了。可以断言说uicollectionview在今后的ios开发中,一定会成为和uitableview一样的强大和最常用的类之一。在ios 6还未正式上市前,先对其特性进行一些学习,以期尽快能使用新特性来简化开发流程,可以说是非常值得的。