使用collectionView實現(xiàn)瀑布流。
系統(tǒng)默認布局
在使用 collectionView 的時候,系統(tǒng)默認提供了 UICollectionViewFlowLayout 流水布局。
在 ViewController 中以懶加載的方式創(chuàng)建 collectionView ,并設置相關的布局。
/// collection View
fileprivate lazy var collectionView: UICollectionView = {
// 使用系統(tǒng)默認的布局
let layout = UICollectionViewFlowLayout()
// 設置每個 section 的內(nèi)容的間距
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// 垂直滾動情況下,表示同一行的兩個item之間的間距
layout.minimumInteritemSpacing = 10
// 垂直滾動情況下,表示連續(xù)行之間的行間距
layout.minimumLineSpacing = 10
let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
collectionView.backgroundColor = UIColor.cyan
return collectionView
}()
在 viewDidLoad() 方法中添加 collectionView。
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
}
此時運行程序,結果如下,只看到青色的背景顏色,并沒有看到其他的什么內(nèi)容。
如果要顯示內(nèi)容,我們需要實現(xiàn) collection view 的數(shù)據(jù)源,在上述的懶加載中設置 collection view 的數(shù)據(jù)源是 ViewController。
...
// 設置數(shù)據(jù)源
collectionView.dataSource = self
return collectionView
設置了數(shù)據(jù)源為控制器之后,控制器就要遵守 UICollectionViewDataSource 協(xié)議,并實現(xiàn)必須的數(shù)據(jù)源方法。
// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
// 返回 item 的個數(shù)
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
// 返回要展示的 cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "kContentCellID", for: indexPath)
cell.backgroundColor = UIColor.red
return cell
}
}
在 viewDidLoad() 方法中使用唯一標識對 cell 進行注冊。
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "kContentCellID")
}
注意,注冊用的唯一標識字符串一定要和數(shù)據(jù)源方法中取出cell用的唯一標識一樣。為了安全起見,把這個標識符設置為一個常量,并在注冊和獲取的地方更新代碼寫法。
/// cell 唯一標識
private let kContentCellID = "kContentCellID"
class ViewController: UIViewController {
// ...
}
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: kContentCellID)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kContentCellID, for: indexPath)
運行程序,此時就可以看到系統(tǒng)默認的布局的顯示方式
流水布局
如果要實現(xiàn)流水布局,就必須自定義布局。創(chuàng)建 HLWaterfallLayout.swift 文件,定義 HLWaterfallLayout 類,并繼承 UICollectionViewFlowLayout。
class HLWaterfallLayout: UICollectionViewFlowLayout {
...
}
準備布局
重寫 prepare() 方法,這個方法告訴 layout 對象更新當前的布局,它的默認實現(xiàn)不執(zhí)行任何操作。子類可以覆蓋它,并使用它來設置數(shù)據(jù)結構或執(zhí)行任何初始計算,以便稍后執(zhí)行布局。
/// 準備布局
override func prepare() {
super.prepare()
}
在這個方法設置 collection view 的 item 的一些屬性,主要就是設置 item 的frame。而 item 的屬性是由 UICollectionViewLayoutAttributes 對象管理的,也就是說每一個 item (cell) 對應著一個 UICollectionViewLayoutAttributes 對象。創(chuàng)建 UICollectionViewLayoutAttributes 對象,并設置該對象的 frame,就是對 cell 進行布局。
// 創(chuàng)建 UICollectionViewLayoutAttributes
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
需要參數(shù) indexPath, 創(chuàng)建indexPath
// 創(chuàng)建 indexPath
let indexPath = IndexPath(item: i, section: 0)
需要參數(shù) i ,也就是具體到每一個cell的索引,此時需要知道item的個數(shù)。
// 獲取 cell 的個數(shù)
let cellCount = collectionView!.numberOfItems(inSection: 0)
for i in 0..<cellCount {
// 創(chuàng)建 indexPath
let indexPath = IndexPath(item: i, section: 0)
// 創(chuàng)建 UICollectionViewLayoutAttributes
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
}
設置 attr 的 frame 屬性,frame 屬性決定了 cell 的布局方式。
定義變量來記錄列數(shù),定義數(shù)組來記錄每一列的高度。
/// 列數(shù)
fileprivate lazy var cols: Int = 3
/// 每一列的高度
fileprivate lazy var cellMaxYs: [CGFloat] = Array(repeating: self.sectionInset.top, count: self.cols)
根據(jù)要展示的列數(shù)來計算cell的寬度,設置cell的高度為隨機值。cell的Y值為列中高度最低的列加上行間距。cell的X值是列中高度最低的列所在的X值。設置frame之后,更新數(shù)組中最低高度列的值。
let cellW: CGFloat = (collectionView!.bounds.size.width - sectionInset.left - sectionInset.right - CGFloat(cols - 1) * minimumInteritemSpacing) / CGFloat(cols)
let cellH: CGFloat = CGFloat(arc4random_uniform(100)) + 50.0
// 找到所有列中的高度最小的Y值
let minMaxY = cellMaxYs.min()!
let minYIndex = cellMaxYs.index(of: minMaxY)!
let cellX: CGFloat = sectionInset.left + (cellW + minimumInteritemSpacing) * CGFloat(minYIndex)
let cellY: CGFloat = minMaxY + minimumLineSpacing
attr.frame = CGRect(x: cellX, y: cellY, width: cellW, height: cellH)
// 更新數(shù)組中的目前高度最低的Y值
cellMaxYs[minYIndex] = minMaxY + minimumLineSpacing + cellH
定義數(shù)組保存 attr
/// cell 對應的 UICollectionViewLayoutAttributes
fileprivate lazy var layoutAttrs: [UICollectionViewLayoutAttributes] = []
// 保存 attr
layoutAttrs.append(attr)
準備布局的最終代碼如下所示
override func prepare() {
super.prepare()
let cellW: CGFloat = (collectionView!.bounds.size.width - sectionInset.left - sectionInset.right - CGFloat(cols - 1) * minimumInteritemSpacing) / CGFloat(cols)
// 獲取 cell 的個數(shù)
let cellCount = collectionView!.numberOfItems(inSection: 0)
for i in 0..<cellCount {
// 創(chuàng)建 indexPath
let indexPath = IndexPath(item: i, section: 0)
// 創(chuàng)建 UICollectionViewLayoutAttributes
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let cellH: CGFloat = CGFloat(arc4random_uniform(100)) + 50.0
// 找到所有列中的高度最小的Y值
let minMaxY = cellMaxYs.min()!
let minYIndex = cellMaxYs.index(of: minMaxY)!
let cellX: CGFloat = sectionInset.left + (cellW + minimumInteritemSpacing) * CGFloat(minYIndex)
let cellY: CGFloat = minMaxY + minimumLineSpacing
attr.frame = CGRect(x: cellX, y: cellY, width: cellW, height: cellH)
// 保存 attr
layoutAttrs.append(attr)
// 更新數(shù)組中的目前高度最低的Y值
cellMaxYs[minYIndex] = minMaxY + minimumLineSpacing + cellH
}
}
返回所有布局
設置好所有的 UICollectionViewLayoutAttributes 之后,要把布局信息返回給系統(tǒng),重寫 layoutAttributesForElements 方法。
/// 返回所有布局
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttrs
}
設置 contentSize
重寫 collectionViewContentSize 屬性來設置 collection view 的滾動范圍
// 設置 contentSize
override var collectionViewContentSize: CGSize {
let h = cellMaxYs.max()! + sectionInset.bottom
return CGSize(width: 0, height: h)
}
使用 HLWaterfallLayout
在 ViewController.swift 中,使用 HLWaterfallLayout 布局。
let layout = HLWaterfallLayout()
運行程序,流水布局已初步完成。
相關代碼: HLWaterfall