iOS开发进阶 - 自定义UICollectionViewLayout实现瀑布流布局

最近项目中需要用到瀑布流的效果,但是用UICollectionViewFlowLayout又达不到效果,自己动手写了一个瀑布流的layout,下面是我的心路路程

先先上效果图与demo地址

因为是用UICollectionView来实现瀑布流的,决定继承UICollectionViewLayout来自定义一个layout来实现一个简单瀑布流的布局,下面是需要重写的方法:

  • 重写这个属性得出UICollectionView的ContentSize:collectionViewContentSize
  • 重写这个方法来得到每个item的布局:layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
  • 重写这个方法给UICollectionView所有item的布局:layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
  • 重写这个方法来实现UICollectionView前的操作:prepare()

实现思路

通过代理模式获得到需要的列数和每一item的高度,用过列数与列之间的间隔和UICollectionView的宽度来得出每一列的宽度,item从左边到右布局,下一列的item放到高度最小的列下面,防止每列的高度不均匀,下面贴上代码和注释:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import UIKit

@objc protocol WCLWaterFallLayoutDelegate {
//waterFall的列数
func columnOfWaterFall(_ collectionView: UICollectionView) -> Int
//每个item的高度
func waterFall(_ collectionView: UICollectionView, layout waterFallLayout: WCLWaterFallLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
}

class WCLWaterFallLayout: UICollectionViewLayout {

//代理
weak var delegate: WCLWaterFallLayoutDelegate?
//行间距
@IBInspectable var lineSpacing: CGFloat = 0
//列间距
@IBInspectable var columnSpacing: CGFloat = 0
//section的top
@IBInspectable var sectionTop: CGFloat = 0 {
willSet {
sectionInsets.top = newValue
}
}
//section的Bottom
@IBInspectable var sectionBottom: CGFloat = 0 {
willSet {
sectionInsets.bottom = newValue
}
}
//section的left
@IBInspectable var sectionLeft: CGFloat = 0 {
willSet {
sectionInsets.left = newValue
}
}
//section的right
@IBInspectable var sectionRight: CGFloat = 0 {
willSet {
sectionInsets.right = newValue
}
}
//section的Insets
@IBInspectable var sectionInsets: UIEdgeInsets = UIEdgeInsets.zero
//每行对应的高度
private var columnHeights: [Int: CGFloat] = [Int: CGFloat]()
private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()

//MARK: Initial Methods
init(lineSpacing: CGFloat, columnSpacing: CGFloat, sectionInsets: UIEdgeInsets) {
super.init()
self.lineSpacing = lineSpacing
self.columnSpacing = columnSpacing
self.sectionInsets = sectionInsets
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

//MARK: Public Methods


//MARK: Override
override var collectionViewContentSize: CGSize {
var maxHeight: CGFloat = 0
for height in columnHeights.values {
if height > maxHeight {
maxHeight = height
}
}
return CGSize.init(width: collectionView?.frame.width ?? 0, height: maxHeight + sectionInsets.bottom)
}

override func prepare() {
super.prepare()
guard collectionView != nil else {
return
}
if let columnCount = delegate?.columnOfWaterFall(collectionView!) {
for i in 0..<columnCount {
columnHeights[i] = sectionInsets.top
}
}
let itemCount = collectionView!.numberOfItems(inSection: 0)
attributes.removeAll()
for i in 0..<itemCount {
if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
attributes.append(att)
}
}
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let collectionView = collectionView {
//根据indexPath获取item的attributes
let att = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
//获取collectionView的宽度
let width = collectionView.frame.width
if let columnCount = delegate?.columnOfWaterFall(collectionView) {
guard columnCount > 0 else {
return nil
}
//item的宽度 = (collectionView的宽度 - 内边距与列间距) / 列数
let totalWidth = (width - sectionInsets.left - sectionInsets.right - (CGFloat(columnCount) - 1) * columnSpacing)
let itemWidth = totalWidth / CGFloat(columnCount)
//获取item的高度,由外界计算得到
let itemHeight = delegate?.waterFall(collectionView, layout: self, heightForItemAt: indexPath) ?? 0
//找出最短的那一列
var minIndex = 0
for column in columnHeights {
if column.value < columnHeights[minIndex] ?? 0 {
minIndex = column.key
}
}
//根据最短列的列数计算item的x值
let itemX = sectionInsets.left + (columnSpacing + itemWidth) * CGFloat(minIndex)
//item的y值 = 最短列的最大y值 + 行间距
let itemY = (columnHeights[minIndex] ?? 0) + lineSpacing
//设置attributes的frame
att.frame = CGRect.init(x: itemX, y: itemY, width: itemWidth, height: itemHeight)
//更新字典中的最大y值
columnHeights[minIndex] = att.frame.maxY
}
return att
}
return nil
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributes
}
}

最后附带demo地址,大家喜欢的话可以star一下

上面是简单的瀑布流的实现过程,希望大家能学到东西,有很多地方考虑的不足,欢迎大家交流学习,谢谢大家的阅读~~


作者 @W_C__L
2016 年 12月 21日

简单不先于复杂,而是在复杂之后~~