文章按照順序?qū)懙?,之前文章寫過的很多邏輯都會略過,建議順序閱讀,并下載源碼結(jié)合閱讀。
目錄
項目下載地址: CollectionView-Note
UICollectionView 01 - 基礎(chǔ)布局篇
UICollectionView 02 - 布局和代理篇
UICollectionView 03 - 自定義布局原理篇
UICollectionView 04 - 卡片布局
UICollectionView 05 - 可伸縮Header
UICollectionView 06 - 瀑布流布局
UICollectionView 07 - 標(biāo)簽布局
前兩篇的自定義布局都是基于系統(tǒng)的 UICollectionViewFlowLayout
然后對其進行一定的修改,但是有的布局用系統(tǒng)的流式布局無法完成,這時候就需要我們自定義。比如本篇要實現(xiàn)的瀑布流布局。如下所示。
像這種左邊和右邊不對齊,但是按照一定規(guī)律排列的布局,就需要我們自己這頂規(guī)則,自己決定每個元素應(yīng)該放的位置。
首先創(chuàng)建一個 WaterFallsViewController
使用并以色塊作為數(shù)據(jù)源,像之前幾篇一樣。本篇的重點放在布局上,其他次要代碼前面寫過好幾篇,不再重復(fù),大家可以下載源碼配合查看。
原理篇說過要完全自定義,需要繼承 UICollectionViewLayout
, 有三個必須實現(xiàn)的方法。
override func prepare()
override var collectionViewContentSize: CGSize
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
這里我們創(chuàng)建一個繼承自 UICollectionViewLayout
的 WaterFallsLayout
。定義幾個屬性。
var numberOfColumns = 2
var minimumLineSpacing: CGFloat = 0
var minimumInteritemSpacing: CGFloat = 0
一行需要顯示多少列 , 行間距 , 列間距
這個瀑布流的最大的變量是每個元素的高度是不同的,這里我們給 WaterFallsLayout
添加一個代理方法,高度交給使用者來傳遞
protocol WaterFallsLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForItemAt indexPath: IndexPath) -> CGFloat
}
然后再添加幾個變量
// 代理 用來獲取元素的高度
var delegate: WaterFallsLayoutDelegate?
// 緩存,將計算好的 UICollectionViewLayoutAttributes 存儲起來,防止重復(fù)計算
private var cache = [UICollectionViewLayoutAttributes]()
// 內(nèi)容高度,為collectionViewContentSize 準(zhǔn)備
private var contentHeight: CGFloat = 0
// 寬度 一個計算屬性
private var width: CGFloat {
return collectionView!.bounds.width
}
下面開始在prepare
方法中做一些準(zhǔn)備工作。 并計算每個元素的位置,然后存儲, 本篇只涉及到 cell 的位置自定義 ,下一篇 會加上 SupplementaryView
位置的自定義。
override func prepare() {
// 1
if let collectionView = collectionView, let delegate = delegate , cache.isEmpty {
// 2
let columnWidth = (width - (CGFloat(numberOfColumns) + 1)*minimumInteritemSpacing) / CGFloat(numberOfColumns)
// 3
var xOffsets = [CGFloat]()
for column in 0..<numberOfColumns {
xOffsets.append(CGFloat(column)*columnWidth + minimumInteritemSpacing*CGFloat(column+1))
}
// 4
var yOffsets = [CGFloat](repeating: minimumInteritemSpacing, count: numberOfColumns)
var column = 0
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 5
let height = delegate.collectionView(collectionView, heightForItemAt: indexPath)
let frame = CGRect(x: xOffsets[column], y: yOffsets[column], width: columnWidth, height: height)
// 6
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
// 7
contentHeight = max(contentHeight, frame.maxY)
// 8
yOffsets[column] = yOffsets[column] + height + minimumInteritemSpacing // 下一個column的高度
// 9
column = column >= (numberOfColumns - 1) ? 0:column+1
}
}
}
我們詳細看下準(zhǔn)備和計算過程。
- 對可選類型進行可選綁定,判斷緩存中沒有數(shù)據(jù) 才做計算,否則直接使用緩存 。
- 用總寬度減去元素間距除以總列數(shù)得到每個元素的 寬度
columnWidth
- 用
xOffsets
存儲列元素的x坐標(biāo)(如果只有兩列 就只存兩個) - 初始化某一列元素的 y 坐標(biāo)為行間距(不是從0開始) , 初始化 column 列從0開始計數(shù) , 對所有元素進行遍歷
- 獲取
indexPath
元素的高度 , 得到當(dāng)前元素的frame - 將上一步計算的frame賦值給
UICollectionViewLayoutAttributes
加入緩存 - 重新設(shè)置
contentHeight
為最大的y坐標(biāo)。 - 前面初始化了
yOffsets
都是從minimumInteritemSpacing
,假如現(xiàn)在計算的是第0列,高度為100 , 那么下一個就刪 上一次的高度 yOffsets[column] + 元素的高度100 (height) + 行間距minimumInteritemSpacing
- 如果此次元素是第0列,下一次就是第一列 ,如果超過最大列數(shù)限制,回到第0列。
ok , 對以上的計算做了非常詳細的解釋,實現(xiàn)另外兩個方法就非常簡單了。
// 1
override var collectionViewContentSize: CGSize {
return CGSize(width: width, height: contentHeight)
}
// 2
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
- 返回
collectionViewContentSize
確定可滾動區(qū)域,這個前面已經(jīng)計算好的。 - 在元素將要展示的時候
collectionView
通過這個方法來找我們要元素的布局信息,我們已經(jīng)緩存好了,直接判斷相交區(qū)域的UICollectionViewLayoutAttributes ,返回即可。
然后像之前幾篇一樣在Storyboard
中替換layout 。 查看效果,如開篇所示。
以上幾十行代碼就實現(xiàn)了一個簡單的自定義瀑布流布局。其實自定義布局不止這些,它還能做更多,如 SupplementaryView
的布局,cell添加刪除的動畫等等。下篇介紹更多特性。