圖片類的應用我們常常會看到所謂的「瀑布流排版」,各種不同大小的圖片拼接擺放在畫面上,而也有人直接稱這種排版為Pinterest排版,
可能是因為Pinterest是早期經典的RWD設計網站之一。而正式一點的說法應該是Masonry Layout,Dynamic Grid Layout。
比如這張Pinterest官網的圖:
動手做「瀑布流排版」
上面是我們最終的實現效果,感覺不錯的話可以繼續往下看:D
排版的邏輯
- 第一排橘色的部分,直接從左至右放下圖片。
- 接下來不斷的將新的圖片,安插在最短的column上,從而實現瀑布流的排版方式,可以參考上面放置圖片的數字順序。
自定Layout
我們打算通過UICollectionViewFlowLayout來實現這個佈局,
prepare()是它的入口,在這裡可以做一些初始化的設定,比如基本的邊界、cell之間的距離等等。
因為demo中是提供了切換佈局的功能,而我們希望佈局在計算過後不用再重新計算,所以會先判斷是否已經算過。
如果沒有計算過則執行我們的computeAndStoreAttributesWithItemWidth方法來計算佈局信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
override func prepare() { super.prepare() minimumInteritemSpacing = 10 minimumLineSpacing = 10 sectionInset.top = 10 sectionInset.left = 10 sectionInset.right = 10 // 如果之前沒有計算過Layout則計算並存入cache中 if layoutAttributes[layoutType.keyName] == nil && collectionView != nil{ // 根據想要的column數量來計算一個cell的寬度 let contentWidth:CGFloat = collectionView!.bounds.size.width - sectionInset.left - sectionInset.right let itemWidth = (contentWidth - minimumInteritemSpacing * (CGFloat(layoutType.column)-1)) / CGFloat(layoutType.column) // 計算cell的佈局 computeAndStoreAttributes(layoutType ,CGFloat(itemWidth)) } } |
「排版的本質是去計算每一個cell在scrollView中的位置」。
下面是計算每一個Cell的Attribute方法,其中包含了計算後存起來的動作,計算後會將結果存起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// 計算cell的frame以及設定item size來提供系統計算UICollectionView的contentSize fileprivate func computeAndStoreAttributes(_ layoutType:LayoutType ,_ itemWidth:CGFloat) { // 以sectionInset.top作為最初始的高度,紀錄每一個column的高度 var columnHeights = [CGFloat](repeating: sectionInset.top, count: layoutType.column) // 記錄每一個column的item個數 var columnItemCount = [Int](repeating: 0, count: layoutType.column) // 紀錄每一個cell的attributes var attributes = [UICollectionViewLayoutAttributes]() var row = 0 for item in items { // 建立一個attribute let indexPath = IndexPath.init(row: row, section: 0) let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath) // 找出最短的Column let minHeight = columnHeights.sorted().first! let minHeightColumn = columnHeights.index(of: minHeight)! // 新的照片放到最短Column上 columnItemCount[minHeightColumn] += 1 let itemX = (itemWidth + minimumInteritemSpacing) * CGFloat(minHeightColumn) + sectionInset.left let itemY = minHeight // 計算高度,按照原圖片大小等比例縮放 let itemHeight = item.size.height * itemWidth / item.size.width // 設定Frame,加入到attributes中 attribute.frame = CGRect(x: itemX, y: CGFloat(itemY), width: itemWidth, height: CGFloat(itemHeight)) attributes.append(attribute) // 計算最短的column當前的高度 columnHeights[minHeightColumn] += itemHeight + minimumLineSpacing row += 1 } // 找出最高的Column let maxHeight = columnHeights.sorted().last! let column = columnHeights.index(of: maxHeight) // 用於系統計算collectionView的contentSize - 根據最高的Column來設置itemSize,使用總高度的平均值 let itemHeight = (maxHeight - minimumLineSpacing * CGFloat(columnItemCount[column!])) / CGFloat(columnItemCount[column!]) itemSize = CGSize(width: itemWidth, height: itemHeight) // 將計算後的結果存起來 layoutAttributes[layoutType.keyName] = attributes layoutItemSize[layoutType.keyName] = itemSize } |
下面是系統讀取attributes的方法,因為layoutAttributes中只有每一個cell的frame資訊,
所以要記得同時修改itemSize,否則因為itemSize的錯誤而導致UICollectionView的contentSize是錯的。
1 2 3 4 5 6 7 8 |
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // 佈局變化時記得跟著改變itemSize if let size = layoutItemSize[layoutType.keyName] { itemSize = size } return layoutAttributes[layoutType.keyName] } |
Demo中其他的效果
UICollectionView切換的動畫的方法,要記得先執行collectionViewLayout.invalidateLayout,然後再換成新的佈局。
1 2 3 4 5 6 7 |
// 更換layout動畫 UIView.animate(withDuration: 1, animations: { [unowned self] in self.aCollectionView.collectionViewLayout.invalidateLayout() self.aCollectionView.performBatchUpdates({ [unowned self] in self.aCollectionView.setCollectionViewLayout(self.flowLayout, animated: true) }, completion: nil) }) |
推薦和參考
- 歡迎分享本文「Swift實現瀑布流排版」,https://ios.devdon.com/archives/593
- 本篇文章的例子已上傳Github,請參考「Swift實現瀑布流排版」。
哈囉~~
請問我run sample code的時候
會有contentSize的高 變得很高很高
這會是什麼原因呢?
請問切換Segment的時候,FlowLayout 的方法 prepare -> layoutAttributesForElements 這個流程為什麽都會跑三次?
Hi Bevis,
collection view第一次出現的時候會調用一次,然後當它被invalidate的時候也會調用一次,而當view有變化時也會被調用一次。
所以你看到三次的調用:
謝謝回复,
關於切Segment時collection view layout 變換的動畫,我將你的代碼刪刪減減變換順序得出來的效果,還是原本的最平滑,但是不太理解為什麼那樣的組合有那樣的效果。
另外,我用instrument 的 leak 查了一下,似乎在變換layout時會產生一個memory leak,我原本以為應該是closure retain住了self、collection view 或是 layout ,但是改了一改leak還是沒消掉。
晚上好:D
抱歉因為光想著實現瀑布流排版這件事情,而疏忽了一些細節。
instrument中提示的memory leak是指HomeFlowLayout.swift文件中的,「fileprivate var layoutItemSize = [String:CGSize]()」方法導致的。
當我從原本的方法改成layz初始化方法以後就消除了memory leak,至於為什麼…目前我還沒想到Orz,需要進一步了解一下。
fileprivate lazy var layoutItemSize:[String:CGSize] = {
return [String:CGSize]()
}()
fileprivate lazy var layoutItemSize:[String:CGSize] = {
return [String:CGSize]()
}
這個寫法是 compute property 的寫法。(註:個人經驗應該可以省略最後的括號)
與原本直接將 [String: CGSize] 給值的方法不同。
原本 var layoutItemSize = [String:CGSize]() 這個寫法會在取值的時候重覆的 init 一個新的 [String: CGSize] 的物件。
或許改寫成 var layoutItemSize: [String: CGSize] = [:] 就可以了,歡迎試試
好方法 👍