前言
这篇文章讲一下CollectionView的高级用法,比如自定义布局
自定义布局
先写个入门的布局代码:
import UIKit
class ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate {
var flowLayout : UICollectionViewFlowLayout?
var collectionView : UICollectionView?
var items : [[String]]?
override func viewDidLoad() {
super.viewDidLoad()
buildData()
flowLayout = UICollectionViewFlowLayout.init()
flowLayout?.minimumLineSpacing = 20
flowLayout?.minimumInteritemSpacing = 10
flowLayout?.itemSize = CGSize(width: 65, height: 35)
flowLayout?.scrollDirection = .vertical
flowLayout?.headerReferenceSize = CGSize(width: 150, height: 50)
flowLayout?.footerReferenceSize = CGSize(width: 130, height: 50)
flowLayout?.sectionInset = .init(top: 10, left: 10, bottom: 10, right: 10)
let screenBounds = UIScreen.main.bounds;
let collectionFrame = CGRect(x: 0, y: 50, width: screenBounds.width, height: screenBounds.height-100)
collectionView = UICollectionView(frame: collectionFrame, collectionViewLayout: flowLayout!)
collectionView?.backgroundColor = .gray
collectionView?.alwaysBounceVertical = true
view.addSubview(collectionView!)
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
collectionView?.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header")
collectionView?.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer")
collectionView?.dataSource = self
collectionView?.delegate = self
}
private func buildData() {
items = [["1:1","1:2","1:3","1:4","1:5","1:6","1:7","1:8","1:9","1:10","1:11","1:12","1:13","1:14","1:15","1:16"],["2:1","2:2","2:3","2:4","2:5","2:6","2:7","2:8","2:9","2:10","2:11","2:12","2:13","2:14","2:15","2:16","2:17","2:18","2:19","2:20"]]
}
//MARK:- data source
func numberOfSections(in collectionView: UICollectionView) -> Int {
return items!.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items![section].count
}
//MARK:- delegate
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .green
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header", for: indexPath)
header.backgroundColor = .blue
return header
} else if kind == UICollectionView.elementKindSectionFooter {
let footer = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer", for: indexPath)
footer.backgroundColor = .orange
return footer
} else {
return UICollectionReusableView()
}
}
}
效果如下:
-
dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath)
这个方法一定会返回一个cell,前提是identifier要注册过,不然抛异常 -
forSupplementaryViewOfKind
的参数虽然是一个String
,但需要传UICollectionView.elementKindSectionHeader
或UICollectionView.elementKindSectionFooter
-
flowLayout
可以直接设置headerReferenceSize/footerReferenceSize
,这是default size,但是如果实现了代理collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int)
,会使用代理返回的值。重点是,如果是vertical滚动,只会用高度,宽度和collectionView一致,horizental滚动反过来 -
UICollectionViewCell
默认没有title
等元素,完全一个白板,需要自己加UI组件(和UITableViewCell
不同,后者默认有title等UI元素
自定义布局
我们都知道要使用CollectionView
离不开两个类:UICollectionView
和UICollectionViewLayout
,其中后者掌握了CollectionView
的布局。实际上,常用的有关布局的代理方法,比如sizeForItem
的调用时机也是在UICollectionViewFlowLayout
的prepare
中,注意,UICollectionViewFlowLayout
自己实现的prepare
中调用了sizeForItem
代理,但是UICollectionViewLayout
是不会自己调用prepare
的,默认的是实现是空,举个例子吧:
重写UICollectionViewFlowLayout
的prepare
方法,并使用这个layout:
class CustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
print("prepare1")
super.prepare()
print("prepare2")
itemSize = CGSize(width: 120, height: 50)
}
}
打印日志:
prepare1 # 调用super.prepare前
numberOfSections
numberOfItemsInSection
numberOfItemsInSection
sizeForItemAt
sizeForItemAt
referenceSizeForHeaderInSection
sizeForItemAt
sizeForItemAt
referenceSizeForHeaderInSection
prepare2 # 调用super.prepare后
cellForItemAt
cellForItemAt
cellForItemAt
cellForItemAt
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
viewForSupplementaryElementOfKind
可以看到UICollectionViewFlowLayout
的prepare
方法调用了numberOfSections
& numberOfItemsInSection
& sizeForItemAt
& referenceSizeForHeaderInSection
等涉及到布局的代理方法
但是UICollectionViewLayout
的prepare
方法的默认实现是空函数
瀑布流Pinterest-Like布局
下面着手写一个瀑布流的demo:
class CustomCollectionLayout: UICollectionViewLayout {
private var attrCache : [UICollectionViewLayoutAttributes] = []
private let columnNum = 2
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
private var contentWidth : CGFloat {
guard let collectionView = collectionView else {
return 0
}
let inset = collectionView.contentInset
return collectionView.bounds.width - (inset.left + inset.right)
}
private var contentHeight : CGFloat = 0
private let cellPadding : CGFloat = 5
override func prepare() {
print("prepare1")
super.prepare()
guard let collectionView = collectionView,
attrCache.count == 0 else {
return
}
var columns : [CGFloat] = Array.init(repeating: 0, count: columnNum)
for idx in 0..<collectionView.numberOfItems(inSection: 0) {
let offsetXIdx = idx % 2
let x = CGFloat(offsetXIdx) * (contentWidth / 2)
let y = columns[offsetXIdx]
let width = contentWidth/2
let height = randomHeight() + cellPadding * 2
var frame = CGRect(x: x, y: y, width: width, height: height)
frame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let indexPath = IndexPath(item: idx, section: 0)
let collectionAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
collectionAttributes.frame = frame
attrCache.append(collectionAttributes)
columns[offsetXIdx] += height
}
contentHeight = columns.max()!
print("prepare2")
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard attrCache.count > 0 else {
return nil
}
return attrCache[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard attrCache.count > 0 else {
return nil
}
var res : [UICollectionViewLayoutAttributes] = []
for attr in attrCache {
if attr.frame.intersects(rect) {
res.append(attr)
}
}
return res
}
private func randomHeight() -> CGFloat {
let randomHeight = CGFloat.random(in: 50...150)
print("randowm:\(randomHeight)")
return randomHeight
}
}
同样的,有以下几点需要注意的:
- 观察
prepare
方法加的log输出,并没有调用sizeForItem
代理方法 -
contentHeight
直到prepare结束才确定的 - 加
cacheAttr
的原因:
demo截图如下:
结交人脉
-
——点击加入:iOS开发交流群
以下资料在群文件可自行下载
驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!**