一、引言
Session 225 《A Tour of UICollectionView》从三方面来对 UICollectionView
进行讨论,分别是 Layouts
、 Updates
以及 Animations
。
在正式开始讨论之前,先了解一下 UICollectionView
的三个重要概念: Layout
、 Data source
以及 Delegate
。
二、UICollectionView
相关重要概念
1. Layout
UICollectionView
的布局有以下几个特点:
- 所有内容显示的信息都被指定在
UICollectionViewLayoutAttributes
中。 - 提供了无效化机制来允许在显示过程中改变布局。
- 当
UICollectionView
改变布局的时候,会显示动画。
UICollectionView
的布局被抽象成了 UICollectionViewLayout
类,该类是一个抽象类,不可直接使用,但系统提供了一个 UICollectionViewLayout
的子类 UICollectionViewFlowLayout
来供开发者使用。
UICollectionViewFlowLayout
UICollectionViewFlowLayout
是UICollectionViewLayout
的子类。UICollectionViewFlowLayout
扩展了UICollectionViewDelegate
。UICollectionViewFlowLayout
是基于线性布局的。
那么 UICollectionViewFlowLayout
是如何基于线性布局的呢?
若布局方向为竖直的,那么 UICollectionViewFlowLayout
的布局方式如下图:
其中,与 mermaid flowchat Layout
相关的两个重要概念为 Line Spacing
和 Inter-Item Spacing
,它们在竖直布局中的表现如下图:
Line Spacing
Inter-Item Spacing
若布局方向为水平,那么 UICollectionViewFlowLayout
的布局方式如下图:
其中, Line Spacing
和 Inter-Item Spacing
在水平布局中的表现如下图:
Line Spacing
Inter-Item Spacing
在 UICollectionViewFlowLayout
中,我们可以指定 Line Spacing
和 Inter-Item Spacing
的最小值。
2. Data source
Layout
指定了 UICollectionView
如何显示内容,而内容则是由 Data source
指定的。
Data source
涉及到的有关方法如下:
// 指定section个数,可选的,默认1个section
optional func numberOfSections(in collectionView: UICollectionView) -> Int
// 指定section中item个数
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
// 指定item显示内容
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
3. Delegate
关于 UICollectionView
的 Delegate
,有以下几个特性:
Delegate
是可选的。UICollectionViewDelegate
是对UIScrollViewDelegate
的拓展。- 可以完成一些细粒度控制,比如对高亮和选中进行控制。
- 可以对内容出现在屏幕过程进行控制,例如
willDisplayItem
和didEndDisplayingItem
。
三、简单使用自定义的 UICollectionViewLayout
对于自定义的 UICollectionViewLayout
,我们只需简单的重写它的 prepareLayout
方法即可。
在 prepareLayout
方法中,我们只需指定 itemSize
、 sectionInset
、 sectionInsetReference
等属性就能完成自定义。
prepareLayout
会在 UICollectionView
布局无效化的时候调用,那么 UICollectionView
无效化如何触发呢?当 UICollectionView
的 bounds
发生变化时(例如改变 UICollectionView
大小,旋转屏幕等),此时会调用 prepareLayout
。
四、完整的自定义 UICollectionViewLayout
要想完整的自定义一个 UICollectionViewLayout
,我们只需要重写4个方法以及根据需求实现一个额外方法就可以了。
1. 重写方法
open var collectionViewContentSize: CGSize { get }
该方法需要提供 UICollectionView
所有内容所占区域大小。
由于 UICollectionView
是继承自 UIScrollView
的,该方法返回值与 UIScrollView
的 contentSize
是类似的。
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
上述两个方法都是需要返回 UICollectionView
中内容布局信息,不同的是第一个方法需要根据区域返回,第二个方法需要根据具体排列位置返回。
以上两个方法在 UICollectionView
滑动过程中会频繁调用,所以为了保证效率,不要在这两个方法中做复杂的数学计算,若无法避免进行计算,则应当考虑使用算法进行优化。
func prepare()
该方法在上面已经提到过了,正如同上面所说,该方法会在布局无效化的时候进行调用,所以在该方法中要做的处理,大致应该是以下几步:
- 重置布局中的缓存数据。
- 计算所有内容的布局。
- 缓存计算好的布局。
- 依据计算好的布局及时更新
contentSize
,使之与布局保持同步。
2. 额外方法
func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
对于该方法,首先需要注意以下几点:
- 每次
UICollectionView
的bounds
发生变化时就会被调用。 - 当
UICollectionView
滚动时会发生调用。 - 默认实现为返回
false
。
该方法会将布局设置为无效,进而调用 prepare
方法,所以在该方法中,根据具体情况进行设置,可以有效的减少 prepare
方法的调用次数,最大可能的提高效率。
五、Update
和Animations
首先假如我们要实现一个更新数据源的功能,在该功能中,我们为功能加上特定的动画,动画效果如下:
在该更新动画中,由以下三个小部分更新动画组成:
- 重新加载最后一条数据内容。
- 将最后一条数据移动至第一条。
- 删除第三条数据。
正常的代码实现应当如下所示:
// MARK: Updates
func performUpdates() {
collectionView.performBatchUpdates({
// Update Data Source
people[3].isUpdated = true
let movedPerson = people[3]
people.remove(at: 3)
people.remove(at: 2)
people.insert(movedPerson, at: 0)
// Update Collection View
collectionView.reloadItem(at: [IndexPath(item: 3, section: 0)])
collectionView.deleteItem(at: [IndexPath(item: 2, section: 0)])
collectionView.moveItem(at: IndexPath(item: 3, section: 0), to: IndexPath(item: 0, section: 0))
})
}
以上代码的实现思路为:调整数据为最终效果所需数据,对应使用动画调整界面元素。
但是很不幸,以上代码会发生如下错误:
为什么会发生如上错误呢?我们首先来看一下performBatchUpdates
方法的一些特点:
- 所有动画会一起执行。
- 在闭包中执行数据源的更新以及CollectionView视图的更新。
- CollectionView视图的更新顺序不重要。
- 数据源的更新顺序很重要。
既然提到了CollectionView视图的更新顺序不重要,那么出现问题的原因就在于数据源的更新顺序上了。
那我们看一下视图更新与数据源更新之间有什么关系:
由CollectionView的每个Action操作含义我们可以得出以下会引起错误的操作:
- Move和Delete同一位置元素。
- Move和Insert同一位置元素。
- Move多个元素到统一位置。
- 使用错误的位置元素。
那么我们应该如何避免出现以上错误操作呢?
- 将Move操作分解为Delete和Insert操作。
- 合并所有Delete和Insert操作。
- 先处理Delete操作,它们是按照降序处理的。
- 最后处理Insert操作,它们是按照升序处理的。
那对于reloadData
来说,有什么特点呢:
- 无需更新操作。
- 能够同步数据源和CollectionView。
- 不需要动画。
根据以上建议,我们重新实现一下开始时候提到的需求了。
// MARK: Updates
func performUpdates() {
// Perform reloads first
UIView.performWithoutAnimation {
collectionView.performBatchUpdates({
people[3].isUpdated = true
collectionView.reloadItems(at: [IndexPath(item: 3, section: 0)])
})
}
collectionView.performBatchUpdates({
// We have 2 updates:
// - delete item at index 2
// - Move item at index 3 to index 0
// becomes...
// delete item at index 2
// delete item at index 3
// insert item from index 3 at index 0
let movedPerson = people[3]
people.remove(at: 3)
people.remove(at: 2)
people.insert(movedPerson, at: 0)
// Update Collection View
collectionView.deleteItems(at: [IndexPath(item: 2, section: 0)])
collectionView.moveItem(at: IndexPath(item: 3, section: 0), to: IndexPath(item: 0, section: 0))
})
}
以上就是关于该篇Session提到的UICollectionView
使用过程中的一些技巧及注意事项,如有遗留,欢迎补充。