iOS UICollectionView 橫向分頁佈局

一個絕望的氣純發表於2018-07-12

有一種流行的 UICollectionView 橫向分頁佈局方式,當你佈局好 itemSize 等資訊後,興沖沖的執行模擬器時,卻發現結果是這樣的:

iOS UICollectionView 橫向分頁佈局

此時你想要的佈局方式應該是這樣:

iOS UICollectionView 橫向分頁佈局

那我們就需要重新自定義 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 的話),就像這樣:

iOS UICollectionView 橫向分頁佈局

你可能碰上了類似這樣的事情,後面分頁的 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)
}
複製程式碼

效果

iOS UICollectionView 橫向分頁佈局

iOS UICollectionView 橫向分頁佈局

完整 Demo

github.com/HjzCy/UICol…

這是我的一些思考方式,如果您有更好的方法、或看到寫的不好的地方,歡迎留言探討。

相關文章