20、UICollectionView的使用
实现垂直方向的单列表来说,使用UITableView
足以;
若是需要构建横向滑动列表、gridView等直线型布局,则使用UICollectionView+UICollectionViewFlowLayout
搭建最合适;
更复杂的布局,则可以使用UICollectionView
+自定义Layout
来实现。
UICollectionView的设计理念
UICollectionView
是内容和布局完全分离的设计,UICollectionView
负责界面部分,UICollectionViewlayout
负责UICollectionView
的布局,具体的每个元素的布局就交给UICollectionViewLayoutAttributes
,另外attributes
也是可以进行扩展的,比如需要加入maskView
或者改变layer
的属性,都可以在attributes
里面进行自己的定义。
UICollectionView的工作流程
当UICollectionView
显示内容时,先从数据源获取cell
,然后交给UICollectionView
。再从UICollectionViewLayout
获取对应的layout attributes
(布局属性)。最后,根据每个cell
对应的layout attributes
(布局属性)来对cell
进行布局,生成了最终的界面。而用户交互的时候,都是通过Delegate
来进行交互。当然,上面只是布局cell
,但是UICollectionView
内部还有Supplementary View
和Decoration View
,也可以对其进行布局。
UICollectionViewFlowLayout的用法
UICollectionViewFlowLayout常用的属性
itemSize
:如果cell的大小是固定的,应该直接设置此属性,就不用实现设置大小的代理方法了。minimumLineSpacing
:行之间的最小间距。minimumInteritemSpacing
:最小cell之间的间距。sectionInset
:每一组的内容缩进scrollDirection
:设置滚动方向headerReferenceSize
:header参考大小footerReferenceSize
:footer参考大小sectionHeadersPinToVisibleBounds
: 顶部是否悬停sectionFootersPinToVisibleBounds
: 底部是否悬停
常用方法
prepare
prepare()
是专门用来准备布局的,在prepare方法里面我们可以事先就计算后面要用到的布局信息并存储起来,防止后面方法多次计算,提高性能。例如,我们可以在此方法就计算好每个cell的属性、整个CollectionView
的内容尺寸等等。此方法在布局之前会调用一次,之后只有在调用invalidateLayout、shouldInvalidateLayoutForBoundsChange:返回YES和UICollectionView刷新的时候才会调用。
//预加载布局属性
override func prepare() {
super.prepare()
}
layoutAttributesForItem
返回对应的indexPath的cell的attributes。
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
}
layoutAttributesForSupplementaryView
返回对应的header和footer的attributes
layoutAttributesForSupplementaryView
collectionViewContentSize
collectionView的size 这个size不是可视范围的size是整个collectionView的size
layoutAttributesForElements
返回在rect范围内所有cell footer和head的attribute
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
}
自定义FlowLayout
理解Layout的布局过程
invalidateLayout
会使当前的 layout
无效,并触发 layout
的更新,会强迫 layout
对象重新计算 layout
属性。与 reloadData
不一样,当 data source
中的数据发生改变,适合用 reloadData
方法。
在布局过程中,会按顺序调用一下函数,可以在这些方法中计算 item 的位置信息。
prepare
方法来执行一些准备工作,可以进行一些 layout 布局需要的计算等collectionViewContentSize
方法用来返回整个内容的 content size 大小layoutAttributesForElements
: 方法用来返回在指定的矩形区域内的 cells 的属性等信息
prepare
方法是为确定布局中各 cell
和 view
位置做计算,需要在此方法中算出足够的信息以供后续方法计算内容区域的整体 size
,collection view
使用 content size
以正确地配置 scroll view
。比如 content size
长宽均超过屏幕的话,水平与竖直方向的滚动都会被 enable
。基于当前滚动位置,collection view
会调用 layoutAttributesForElements:
方法以请求特定 rect
(有可能是也可能不是可见 rect)中 cell
和 view
的属性。到此,core layout process
已经结束了。
layout
结束之后,cells
和views
的属性在你或者 collection view invalidate
布局之前都不会变。调用 invalidateLayout
方法会导致新的一次 layout process
开始,以调用 prepare
方法开始。collection view
可以在滚动的过程中自动 invalidate
布局,用户滚动内容过程中,collection view
调用layout
的 shouldInvalidateLayoutForBoundsChange
: 方法,如果返回值为 YES
则 invalidate
布局。(但需要知道的是,invalidateLayout
并不会马上触发 layout update process
,而是在下一个view
更新周期中,collection view
发现 layout
已经 dirty
才会去更新)
创建布局属性Layout Attributes
自定义 layout
需要返回一个 UICollectionViewLayoutAttributes
类的对象,这些对象可以在很多不同的方法中创建,但创建时间可以根据具体情况决定。
如果 collection view
不会处理上千个 items
时,则 prepareLayout
创建会比用户滚动过程中用到时在计算更高效,因为创建的属性可以缓存起来。如果计算所有属性并缓存起来所带来的性能消耗比请求时在计算属性的消耗更大,则可以在请求的时候在计算相关属性。
UICollectionViewLayoutAttributes
的属性:
- frame
- bounds
- center
- size
- transform3D
- transform
- alpha
- zIndex
- hidden
创建 UICollectionViewLayoutAttributes
类对象时,可以使用一些方法:
init(forCellWithIndexPath indexPath: NSIndexPath)
init(forSupplementaryViewOfKind:withIndexPath:)
init(forDecorationViewOfKind:withIndexPath:)
view
的类型不同,必须使用正确的类方法,因为 collection view
会根据这些信息向 data source
对象请求适当类型的 view
,使用错误的方法在错误的地方创建错误的 view
。
创建每个属性对象后,要将相应的 view 的相关属性设置上。最基本的要设置 view 的 size 和 position 信息。如果布局中有 view 重叠了,需要配置正确的 zIndex 属性来维持有序的状态。其他属性可以控制 cell 或 view 的外观及可见性。
准备Layout
在一个布局周期中,首先会调用 prepareLayout
方法,可以来执行一些准备工作,可以进行一些 layout
布局需要的计算等,可以存储一些 layout attributes
信息。
给定矩形中的 items 布局属性
layout process
的最后,collection view
会调用 layoutAttributesForElementsInRect
: 方法,对于一个大的可滚动内容区域,collection view
可能只会请求当前可见的那部分区域中的所有 items
属性。这个方法支持获取任意 rect
中 items
的信息,因为有可能在插入及删除时做动画效果。
layoutAttributesForElementsInRect:
方法实现需要尊重如下的步骤:
- 遍历 prepareLayout 方法产生的数据以访问缓存的属性或创建新的属性
- 检查每个 item 中的 frame 以确定是否与 layoutAttributesForElementsInRect: 方法中指定的 rect 有重叠部分
- 对每个重叠的 item ,添加一个对应的 UICollectionViewLayoutAttributes 对象到一个数组中
- 返回布局属性的数组给 collection view
不仅要记住缓存layout
信息能够带来性能提升,也要记住不断重复为cells
创建新layout
属性的计算代价是十分昂贵的,足以影响到app的性能。当collection view
管理的items量很大时,采用在请求时创建layout
属性的方式是十分合理的。
按需提供布局属性
collection view
会在正常的 layout
过程之外周期性的让你提供单个 items
的layout
对象。比如为某 item
配置插入和删除对话。通过以下方法提供信息:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:
layoutAttributesForItemAtIndexPath:
所有自定义的 layout 必须重写的方法。
当返回属性时,不应该更新这些 layout
属性,如果需要改变 layout
信息,调用 invalidateLayout
在接下来的 layout
周期中更新这些信息。
两种方式设置 collection view
的 layout
为自定义的 layout
,
- 一种方式在 storyboard 文件中,在
Attributes inspector
中设置Layout
从Flow
改为Custom
。 - 另外一种直接代码设置
self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];
插入和删除动画
插入新的 cell 的时候,collection view
会询问 layout
对象提供一组初始化属性用于动画,结束属性就是默认的位置、属性等。类似的,当删除一个 cell 的时候,collection view
会询问 layout
对象提供一组终值属性用于动画,初始属性默认的 indexPath
位置等。
当插入 item
的时候,layout
对象需要提供正在要被插入的 item
的初始化 layout
信息。在此例中, layout
先将 cell
的初始位置位置到 collection view
的中间,并将 alpha
设为0,动画期间,此 cell
会渐渐出现并移动到右下角。参考代码:
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
CGSize size = [self collectionView].frame.size;
attributes.center = CGPointMake(size.width / 2.0, size.height / 2.0);
return attributes;
}
需要注意的是,上述代码在插入 cell
的时候所有的 cell
都会添加此插入的动画,若只想对插入的 item
做插入动画,可以检查 indexPath
是否与传入的prepareForCollectionViewUpdates
: 方法的 item
的 indexPath 匹配,并且只有在匹配的时候才进行动画,否则只返回 super 的initialLayoutAttributesForAppearingItemAtIndexPath
:. 方法。
override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) {
super.prepareForCollectionViewUpdates(updateItems)
insertIndexPath = [NSIndexPath]()
deleteIndexPath = [NSIndexPath]()
for update in updateItems {
switch update.updateAction {
case .Insert:
insertIndexPath.append(update.indexPathAfterUpdate!)
case .Delete:
deleteIndexPath.append(update.indexPathBeforeUpdate!)
default:
print("error")
}
if update.updateAction == UICollectionUpdateAction.Insert {
}
}
}
delete 动画与插入类似,需要提供正确的 final layout 属性。
提升 layout 的滚动体验
当滚动相关的 touch
事件结束后,scrollview
会根据当前的 speed
和减速状况决定最终会停在哪个偏移。一旦 collection view
知道这个位置后,它就会询问 layout 对象是否修改这个位置,通过调用 targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
。由于是在滚动过程中调用此方法,所以自定义 layout
可以改变滚动的停止位置。
假如 collection view
开始于(0,0),且用户向左滑动,collection view
计算出滚动原本会停在如下的位置,这个值是 “proposed” content
的 offset
值。自定义 layout
可以改变这个值,以确保滚动停下的时候,某个 item
正好停留在可见区域的正中间。这个新值会成为新的目标的 content offset
,这个值从 targetContentOffsetForProposedContentOffset:withScrollingVelocity:
方法中返回。
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
//计算最终显示的矩形框
var rect: CGRect = CGRectZero
rect.origin.y = 0
rect.origin.x = proposedContentOffset.x
rect.size = (collectionView!.frame.size)
//根据最终的矩形来获得super已经计算好的属性
let originArray = super.layoutAttributesForElementsInRect(rect)
let attributes = NSArray(array: originArray!, copyItems: true) as? [UICollectionViewLayoutAttributes]
//计算collectionView最中心点的x值
let centerX = proposedContentOffset.x + collectionView!.frame.size.width * 0.5
//存放做小间距
var minDelta: CGFloat = CGFloat(MAXFLOAT)
for attrs in attributes! {
if abs(minDelta) > abs(attrs.center.x - centerX) {
minDelta = attrs.center.x - centerX
}
}
//修改原有的偏移量
return CGPointMake(proposedContentOffset.x + minDelta, proposedContentOffset.y)
}
改进自定义布局的建议
items
数量较少时,数百个或者items layout
信息变化较小时,可以在prepareLayout
中创建并缓存UICollectionViewLayoutAttributes
布局属性对象信息;当items
数量达到上千个时候,需要衡量缓存和重新计算两种方式的性能差异- 禁止继承 UICollectionView
- 不要在
layoutAttributesForElementsInRect:
方法中调用UCollectionView
的visibleCells
方法,因为其实这个调用是转化成了向layout
对象请求visible cells
方法。