有一種流行的 UICollectionView
橫向分頁佈局方式,當你佈局好 itemSize
等資訊後,興沖沖的執行模擬器時,卻發現結果是這樣的:
此時你想要的佈局方式應該是這樣:
那我們就需要重新自定義 UICollectionViewFlowLayout
。
網上看了一些寫法,不過大部分都是相同的,由於看不懂(智商太低了),而且網上的寫法沒能解決我後面的一個問題(後面會說到,分頁處的 item x
不對的問題),於是自己思考寫了一個。不足之處,還望指正。
重新佈局
新建一個繼承於 UICollectionViewFlowLayout
的類,定義好基本資訊:
self.collectionView?.isPagingEnabled = true
複製程式碼
final class ItemFlowLayout: UICollectionViewFlowLayout {
/// 包含了所有重新佈局的 item,在代理方法中,需要返回這個我們自己包裝的陣列。
private lazy var allAttrs = [UICollectionViewLayoutAttributes]()
/// 圖片大小
private lazy var iconSize = #imageLiteral(resourceName: "惠整形").size
/// item 間隔
private var space: CGFloat {
return (collectionView!.frame.width - iconSize.width * 5) / 6
}
/// 設定分頁大小
override var collectionViewContentSize: CGSize {
return CGSize(width: screenWidth * 2, height: iconSize.height + 17 + 5)
}
override init() {
super.init()
itemSize = CGSize(width: iconSize.width, height: iconSize.height + 17 + 5)
scrollDirection = .horizontal
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
複製程式碼
已就緒,此時需要思考的就是上面的兩種佈局方式應該如何轉換過來,而且還能開啟分頁模式,不過開啟分頁模式比較簡單,在上面的程式碼中,我已經重寫了一個系統屬性:
/// 設定分頁大小
override var collectionViewContentSize: CGSize {
return CGSize(width: screenWidth * 2, height: iconSize.height + 17 + 5)
}
複製程式碼
這是個只讀屬性,只能用重寫的方式設定。設定後再次執行就發現可以開啟分頁模式了。
思想很重要
每一種模式的背後都有其核心思想,這個佈局方式就是要我們思考他背後的原理,想一下他整體的大概佈局方式,要怎麼樣才能做到,只要有一點靈感就試一下,我也是慢慢試了才成功的。
這個佈局我的想法是:
既然每一個分頁有 10 個 item ( perRowCount * rowCount
),那我就把每一個分頁的所有 item 都遍歷出來,然後再一一為其設定 x, y
就好了:
// CollectionView 準備佈局時呼叫
override func prepare() {
super.prepare()
// 每行顯示的 item 個數
let perRowCount = 5
// 每個分頁有幾行
let rowCount = 2
// 遍歷一個組的所有 item
(0..<self.collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍歷出每個分頁的 item 索引
let index = item % (perRowCount * rowCount)
// 在 0 -> 9 (perRowCount * rowCount) 之間迴圈
// 輸出: 0, 1, 2... 0,1
// 正好對應上我們每一個分頁的 item 數量,有了這個想法之後,
// 我就想,既然這樣,那我就 < 5 的時候佈局上面的,否則就佈局下面的,不就行了嗎?
print(index)
// 取出預設的 item 佈局資訊。
// attrs 有 frame 屬性,可以修改其 x, y 重新佈局。
let attrs = self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))!
self.allAttrs.append(attrs)
}
}
/// 返回當前可以顯示的 item,
/// 該方法會多次呼叫,所以不能把新增陣列的程式碼寫在這裡。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return allAttrs.filter { rect.contains($0.frame) }
}
複製程式碼
佈局 x, y
// 為了方便,我建立一個臨時索引來計算 x 的值
var z = 0
(0..<collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍歷出每個分頁的 item 索引
let index = item % (perRowCount * rowCount)
let attrs = self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))!
if index < perRowCount { /// 設定上面的 origin
// 沒什麼好說的,y 的話都是 space
// 相當於 inset.top
attrs.frame.origin.y = space
// 如果不懂的話我們慢慢解釋一下:
// space + itemSize.width * CGFloat(z)
// 如果只是這樣的話相信你還是能看得懂的,我們在迴圈裡經常這樣寫,
// 這樣的話除了開始處的 x 有間隔外,其餘 item 都是緊緊貼著的,
// 我們在其後面加上: space * CGFloat(z) 來分開。
// 這樣的話就每個 item 之間都有間隔了。
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z)
z += 1
}else { /// 設定下面 item 的 origin
// 這裡我本來也想像上面那樣建立一個臨時變數來計算 x,
// 但是發現這種寫法正好得出下面的 index
// 其結果是: 0, 1, 2...
let index = item - z
// 隔兩個間距的高度和本身的高,正好得出下面 y 的所在位置。
attrs.frame.origin.y = space * 2 + itemSize.height
// x 和上面一樣,一直乘上去就好了。
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index)
}
self.allAttrs.append(attrs)
}
複製程式碼
這樣基本就好了,但是你執行後會發現有問題(如果你有多餘的 item 的話),就像這樣:
你可能碰上了類似這樣的事情,後面分頁的 x
不對了,沒有和開始處間隔開來。相信是什麼問題你應該知道了。就是我們前面設定了 x
的問題,我們忽略了分頁後的 x
,此時也應該也要偏移一下的。要怎麼辦呢?
通過思考有了一個想法:
既然每個 item 之間通過 + space * CGFloat(z)
之後能夠計算出其間隔,那麼是不是也可以通過 +..curpage * xx
之類的計算出其分頁的間隔呢?
於是試了一下,發現這樣的程式碼可行:
var curpage = 0
(0..<collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍歷出每個分頁的 item 索引
let index = item % (perRowCount * rowCount)
// 得出當前所在分頁
// 忽略掉第 0 頁,並且在每一次 index 等於 0 時,計算出當前所在的是第幾分頁.
// 因為 index 每一次等於 0,都是一次新的分頁。
if item != 0 && index == 0 { curpage += 1 }
if index < perRowCount {
// 在前面追加: `+ space * CGFloat(curapge)`
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z + curpage) + space * CGFloat(curapge)
}else {
let index = item - z
// 在前面追加: `+ space * CGFloat(curapge)`
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index + curpage) + space * CGFloat(curapge)
}
}
複製程式碼
這樣就算好了,再次執行模擬器發現每個 item 的偏移應該是正確的。
為了簡單,我們可以再簡化下賦值 x
的寫法:
if index < perRowCount {
attrs.frame.origin.y = space
// 後面改成: `+ space * CGFloat(z + curpage)`
// 其結果是一樣的。
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z + curpage)
z += 1
}else {
let index = item - z
attrs.frame.origin.y = space * 2 + itemSize.height
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index + curpage)
}
複製程式碼
效果
完整 Demo
這是我的一些思考方式,如果您有更好的方法、或看到寫的不好的地方,歡迎留言探討。