文章按照順序?qū)懙淖柚祝拔恼聦戇^的很多邏輯都會略過友瘤,建議順序閱讀图呢,并下載源碼結(jié)合閱讀。
目錄
項(xiàng)目下載地址: CollectionView-Note
UICollectionView 01 - 基礎(chǔ)布局篇
UICollectionView 02 - 布局和代理篇
UICollectionView 03 - 自定義布局原理篇
UICollectionView 04 - 卡片布局
UICollectionView 05 - 可伸縮Header
UICollectionView 06 - 瀑布流布局
UICollectionView 07 - 標(biāo)簽布局
上一篇的瀑布流只針對cell的自定義布局腰素,這篇為了全面標(biāo)簽布局針對了 整體Header 、sectionHeader 和 cell都做了布局仁烹。 SupplementaryView
跟 tableview
的 header和footer不同耸弄, 我們一個(gè)section可以有任意多個(gè)SupplementaryView
,我們可以自己管理他們的位置卓缰。本篇需要實(shí)現(xiàn)的效果如下
所有顏色都使用隨機(jī)色计呈,標(biāo)簽根據(jù)文字大小決定砰诵,一行顯示不下自動(dòng)換行。并添加了可伸縮Header和sectionHeader捌显。 對刪除和新增做了自定義動(dòng)畫茁彭。 算一個(gè)比較全面的例子。下面看下實(shí)現(xiàn)邏輯過程扶歪。
首先我們做一些數(shù)據(jù)準(zhǔn)備理肺,這里不是重點(diǎn) 提一下,大家可以下載代碼查看善镰。
// 1
let randomText = "黑發(fā)不知勤學(xué)早白首方悔讀書遲遲日江山麗春風(fēng)花草香杜甫絕句春色滿園關(guān)不住一枝紅杏出墻來葉紹翁游園不值好雨知時(shí)節(jié)當(dāng)春乃發(fā)生杜甫春雨夏天小荷才露尖尖角早有蜻蜓立上頭楊萬里小池接天蓮葉無窮碧映日荷花別樣紅"
// 2
func genernalText() -> String{
let textCount = randomText.count
let randomIndex = arc4random_uniform(UInt32(textCount))
let start = max(0, Int(randomIndex)-7)
let startIndex = randomText.startIndex
let step = arc4random_uniform(5) + 2 // 2到5個(gè)字
let startTextIndex = randomText.index(startIndex, offsetBy: start)
let endTexIndex = randomText.index(startIndex, offsetBy: start + Int(step))
let text = String(randomText[startTextIndex ..< endTexIndex])
return text
}
// 3
func generalTags() -> [[String]]{
var tags1: [String] = []
var tags2: [String] = []
var tags3: [String] = []
for i in 0..<50 {
if i%3 == 0 {
tags1.append(genernalText())
}
if i%2 == 0{
tags2.append(genernalText())
}
tags3.append(genernalText())
}
return [tags1,tags2,tags3]
}
- 聲明一長串文字
- 從長文中隨機(jī)產(chǎn)生一個(gè)2-5個(gè)字的文本
- 因?yàn)槭欠纸M這里生成三組不同長度的數(shù)組 組成一個(gè)二維數(shù)組 作為數(shù)據(jù)源妹萨。
然后像之前章節(jié)一樣 Storyboard
中創(chuàng)建一個(gè) TagViewController
, 聲明 collectionView
炫欺, 新建一個(gè) TagLayout
, 替換自帶的 flowLayout
(具體替換方法參照之前文章)
在我們的 TagLayout
有一個(gè)變數(shù)就是文本的長度乎完,這個(gè)我們可以根據(jù)文本的字體和Text內(nèi)容計(jì)算出。為了使用便捷這里提供一個(gè)代理方法 (建議下載源碼結(jié)合查看)
protocol TagLayoutDelegate: class {
func collectionView(_ collectionView: UICollectionView, TextForItemAt indexPath: IndexPath) -> String
}
根據(jù) indexPath
返回對應(yīng)的文本 品洛。
在 TagLayout
頂部添加一個(gè)枚舉
enum Element {
case cell
case header
case sectionHeader
}
包含了 后面我們要自定義位置的三個(gè)元素
添加一個(gè)變量和常量
// 標(biāo)簽的內(nèi)邊距
var tagInnerMargin: CGFloat = 25
// 元素間距
var itemSpacing: CGFloat = 10
// 行間距
var lineSpacing: CGFloat = 10
// 標(biāo)簽的高度
var itemHeight: CGFloat = 25
// 標(biāo)簽的字體
var itemFont: UIFont = UIFont.systemFont(ofSize: 12)
// header的高度
var headerHeight: CGFloat = 150
// sectionHeader 高度
var sectionHeaderHeight: CGFloat = 50
// header的類型
let headerKind = "ElementTagHeader"
weak var delegate: TagLayoutDelegate?
頂部的header為了區(qū)分系統(tǒng)的 elementKindSectionHeader
我們自定義了一種kind树姨。
然后定義一些私有變量。
// 緩存
private var cache = [Element: [IndexPath: UICollectionViewLayoutAttributes]]()
// 可見區(qū)域
private var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// 內(nèi)容高度
private var contentHeight: CGFloat = 0
// 用來記錄新增的元素
private var insertIndexPaths = [IndexPath]()
// 用來記錄刪除的元素
private var deleteIndexPaths = [IndexPath]()}
// MARK: - 一些計(jì)算屬性 防止編寫冗余代碼
private var collectionViewWidth: CGFloat {
return collectionView!.frame.width
}
本篇中的緩存按照枚舉類型進(jìn)行了區(qū)分桥状,但是實(shí)質(zhì)還是差不多的帽揪。
下面開始具體的布局信息計(jì)算和緩存 。
override func prepare() {
// 1
guard let collectionView = self.collectionView , let delegate = delegate else { return }
let sections = collectionView.numberOfSections
// 2
prepareCache()
contentHeight = 0
// 3
// 可伸縮header
let headerIndexPath = IndexPath(item: 0, section: 0)
let headerAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: headerKind, with: headerIndexPath)
let frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: headerHeight)
headerAttribute.frame = frame
cache[.header]?[headerIndexPath] = headerAttribute
contentHeight = frame.maxY
// 4
for section in 0 ..< sections {
// 處理sectionHeader
let sectionHeaderIndexPath = IndexPath(item: 0, section: section)
// 5
let sectionHeaderAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: sectionHeaderIndexPath)
var sectionOriginY = contentHeight
if section != 0 {
sectionOriginY += lineSpacing
}
let sectionFrame = CGRect(x: 0 , y: sectionOriginY , width: collectionViewWidth , height: sectionHeaderHeight)
sectionHeaderAttribute.frame = sectionFrame
cache[.sectionHeader]?[sectionHeaderIndexPath] = sectionHeaderAttribute
contentHeight = sectionFrame.maxY
// 6
// 處理tag
let rows = collectionView.numberOfItems(inSection: section)
var frame = CGRect(x: 0, y: contentHeight + lineSpacing, width: 0, height: 0)
for item in 0 ..< rows {
let indexPath = IndexPath(item: item, section: section)
// 7
let text = delegate.collectionView(collectionView, TextForItemAt: indexPath)
let tagWidth = self.textWidth(text) + tagInnerMargin
// 8
// 其他
if frame.maxX + tagWidth + itemSpacing*2 > collectionViewWidth {
// 需要換行
frame = CGRect(x: itemSpacing , y: frame.maxY + lineSpacing , width: tagWidth, height: itemHeight)
}else{
frame = CGRect(x: frame.maxX + itemSpacing, y: frame.origin.y , width: tagWidth , height: itemHeight)
}
// 9
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache[.cell]?[indexPath] = attributes
}
// 10
contentHeight = frame.maxY
}
}
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache[.sectionHeader] = [IndexPath: UICollectionViewLayoutAttributes]()
cache[.cell] = [IndexPath: UICollectionViewLayoutAttributes]()
cache[.header] = [IndexPath: UICollectionViewLayoutAttributes]()
}
// 根據(jù)文字 確定label的寬度
private func textWidth(_ text: String) -> CGFloat {
let rect = (text as NSString).boundingRect(with: .zero, options: .usesLineFragmentOrigin, attributes: [.font: self.itemFont], context: nil)
return rect.width
}
這段代碼有點(diǎn)長辅斟,我們一一解釋转晰。
- 可選綁定,并獲取section的數(shù)量
- 一些初始化
- 處理頂部的可伸縮header然后加入緩存并更新
contentHeight
士飒, 這里使用了我們的自定義類型headerKind
挽霉。 - 遍歷section 準(zhǔn)備處理每個(gè)section中的內(nèi)容
- 處理sectionHeader 以系統(tǒng)
elementKindSectionHeader
作為kind 。 并加入緩存更新contentHeight
- 獲取到某個(gè)section對用的cell個(gè)數(shù)变汪。初始化一個(gè)frame以之前的
contentHeight
+ 行間距lineSpacing
起步 - 獲取每個(gè)元素的text 侠坎, 然后計(jì)算出對應(yīng)的寬度,加上內(nèi)邊距得到元素的寬度
tagWidth
- 如果frame的最大x左邊加上此元素的寬度和兩個(gè)元素邊距大于
collectionViewWidth
裙盾,需要換行顯示实胸。否則追加在此行。 重置frame的值 - 將frame值賦值給
UICollectionViewLayoutAttributes
并緩存 - 某個(gè)section的cell計(jì)算完之后用最后一個(gè)元素的frame更新
contentHeight
雖然代碼多番官,但是邏輯并不復(fù)雜庐完。都是一些加減計(jì)算。 ok徘熔,緩存準(zhǔn)備好了 之后的變很輕松了门躯。
// 1
override var collectionViewContentSize: CGSize {
return CGSize(width: collectionViewWidth, height: contentHeight)
}
// 2
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[.cell]?[indexPath]
}
// 3
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
switch elementKind {
case UICollectionView.elementKindSectionHeader:
return cache[.sectionHeader]?[indexPath]
case headerKind:
return cache[.header]?[indexPath]
default:
return nil
}
}
- 返回可滾動(dòng)區(qū)域
collectionViewContentSize
- 返回cell對應(yīng)indexPath的
UICollectionViewLayoutAttributes
- 返回
SupplementaryView
對應(yīng) kind 和 indexPath的UICollectionViewLayoutAttributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (type , elementInfos) in cache {
for (_ , attributes) in elementInfos where attributes.frame.intersects(rect){
// 為可伸縮header
if let deltalY = self.calculateDeltalY() , type == .header {
var headerRect = attributes.frame
headerRect.size.height = headerRect.height + deltalY
headerRect.origin.y = headerRect.origin.y - deltalY
attributes.frame = headerRect
}
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
// 計(jì)算可伸縮高度
private func calculateDeltalY() -> CGFloat?{
guard let collectionView = self.collectionView else { return nil }
let insets = collectionView.contentInset
let offset = collectionView.contentOffset
let minY = -insets.top
if offset.y < minY {
let deltalY = abs(offset.y - minY)
return deltalY
}
return nil
}
layoutAttributesForElements
在前幾篇已經(jīng)用過好多次了,就是item將要展示的時(shí)候?qū)⑺麄兊腢ICollectionViewLayoutAttributes返回酷师。這里結(jié)合了可伸縮Header那篇對header進(jìn)行了處理讶凉。
ok到這里染乌,整個(gè)布局篇已經(jīng)寫好了。我們回到TagViewController
中懂讯,新建一個(gè)cell TagCell
荷憋。 只有一個(gè)label在充滿整個(gè)cell。設(shè)置字體和layout中保持一致 褐望。
class TagCell: UICollectionViewCell {
static let reuseID = "tagCell"
@IBOutlet weak var tagLabel: UILabel!
var value: String = "" {
didSet{
tagLabel.text = value
}
}
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = UIColor.randomColor()
tagLabel.font = UIFont.systemFont(ofSize: 12)
tagLabel.textColor = UIColor.white
}
}
header沿用了之前的勒庄。 在viewDidLoad
中進(jìn)行注冊
collectionView.register(UINib(nibName: "ImageHeaderView", bundle: nil), forSupplementaryViewOfKind: headerKind , withReuseIdentifier: ImageHeaderView.reuseID)
collectionView.register(UINib(nibName: "BasicsHeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: BasicsHeaderView.reuseID)
這里的ImageHeaderView
所使用的kind是layout中定義的,在Controller中添加計(jì)算屬性
var headerKind: String {
return layout?.headerKind ?? ""
}
然后再使用Header的時(shí)候也要區(qū)分對待
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: BasicsHeaderView.reuseID, for: indexPath) as! BasicsHeaderView
view.titleLabel.text = "HEADER -- \(indexPath.section)"
view.backgroundColor = UIColor.randomColor()
return view
case headerKind:
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ImageHeaderView.reuseID, for: indexPath) as! ImageHeaderView
return view
default:
fatalError("No such kind")
}
}
別忘了實(shí)現(xiàn)Layout的代理
// MARK: - TagLayoutDelegate
extension TagViewController: TagLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, TextForItemAt indexPath: IndexPath) -> String {
return tags[indexPath.section][indexPath.row]
}
}
其他的基礎(chǔ)代碼和之前無差瘫里,只是換了數(shù)據(jù)源实蔽。
這時(shí)候運(yùn)行所有布局已經(jīng)完成了。
那么如果添加動(dòng)畫呢谨读?也是非常簡單的盐须,記得我們之前聲明了兩個(gè)變量,存儲新增和刪除的元素
在 TagLayout
中漆腌,添加如下方法 用于記錄新增和刪除的元素。
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
self.insertIndexPaths.removeAll()
self.deleteIndexPaths.removeAll()
for update in updateItems {
switch update.updateAction {
case .insert:
if let indexPath = update.indexPathAfterUpdate {
self.insertIndexPaths.append(indexPath)
}
case .delete:
if let indexPath = update.indexPathBeforeUpdate {
self.deleteIndexPaths.append(indexPath)
}
default:break
}
}
}
然后 用另外兩個(gè)方法去執(zhí)行動(dòng)畫
/// MARK: 動(dòng)畫相關(guān)
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attribute = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) else { return nil }
if self.insertIndexPaths.contains(itemIndexPath) {
attribute.transform = CGAffineTransform.identity.scaledBy(x: 4, y: 4).rotated(by: CGFloat(Double.pi/2))
}
return attribute
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attribute = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) else { return nil }
if self.deleteIndexPaths.contains(itemIndexPath) {
attribute.transform = CGAffineTransform.identity.scaledBy(x: 4, y: 4).rotated(by: CGFloat(Double.pi/2))
}
return attribute
}
initial 和 final的方法還有針對SupplementaryView
的阶冈,本篇并不打算演示闷尿,大家可以自行嘗試。
然后再Storyboard
中拖出兩個(gè)控件處理新增和刪除
@IBAction func addTag(_ sender: Any) {
// 隨機(jī)添加一個(gè)tag
let text = DataManager.shared.genernalText()
tags[0].append(text)
let indexPath = IndexPath(item: tags[0].count - 1, section: 0)
collectionView.insertItems(at: [indexPath])
}
@IBAction func deleteTag(_ sender: Any) {
let count = tags[0].count
if count == 0 {
return
}
let indexPath = IndexPath(item: count - 1, section: 0)
self.tags[0].remove(at: indexPath.row)
collectionView.performBatchUpdates({ [ weak self] in
guard let `self` = self else { return }
self.collectionView.deleteItems(at: [indexPath])
}, completion: nil)
}
collectionView
可以使用 performBatchUpdates
處理一系列的操作女坑,比如新增刪除移動(dòng)等填具。并可以處理回調(diào)。
ok , 運(yùn)行 不出意外應(yīng)該完美匆骗。 有問題的仔細(xì)下載代碼查看劳景。或者評論交流碉就。
本系列完結(jié)盟广。