简单瀑布流布局实现(Swift)

写作目的

前段时间面试,问了瀑布流布局,所以就想到写这篇文章分享一下相关的知识点。
瀑布流布局是一种常见的布局方式,它能够美观、灵活地展示不同高度的素材组合的视图控件。
瀑布流布局的特点就在于,item从上往下排列,每次item的放置位置,都是当前的最短列下方。

主要的类

  • UICollectionView:视图
  • UICollectionViewFlowLayout:布局
  • UICollectionViewLayoutAttributes:item配置

需要重写的系统方法

1
2
3
4
5
6
1) 准备布局(第一次布局和刷新时调用)
override func prepare
2) 设置视图内容的尺寸
override var collectionViewContentSize: CGSize
3) 返回计算好的属性数组
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

准备工作

视图&数据源

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
import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

// 数据源
var dataArr = [Int]()
// 列数
let columnCount:Int = 3;
// 瀑布流布局
var flowLayout: WaterfallFlowLayout!

var collectionView: UICollectionView!


//MARK: - --- 视图已经加载
override func viewDidLoad() {
super.viewDidLoad()
// 数据源,同时也是高度值(实际项目中应根据内容计算高度)
self.dataArr = [300,200,300,400,100,200,300,400,100,200,300,400,100,200,300,400]
self.createUI()
}


//MARK: - --- 创建UI
func createUI(){
// 创建collectionView视图 和 flowLayout布局
self.flowLayout = WaterfallFlowLayout()
self.flowLayout.dataArr = self.dataArr
let rect: CGRect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: SCREEN_WIDTH, height: SCREEN_HEIGHT))
let collectionView = UICollectionView.init(frame: rect, collectionViewLayout:self.flowLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = UIColor.white
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "HomeCell")
self.view.addSubview(collectionView)
self.collectionView = collectionView

// 设置布局属性
self.setWaterfallFlowLayouts()

}

//MARK: - --- 设置item的布局
func setWaterfallFlowLayouts(){
// 设置布局属性
self.flowLayout.columnCount = self.columnCount
// 边界
self.flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
// 设置间距
self.flowLayout.minimumLineSpacing = 10.0
self.flowLayout.minimumInteritemSpacing = 10.0
}

//MARK: - --- delegate,dataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeCell", for: indexPath)
cell.backgroundColor = .red
cell.layer.cornerRadius = 8
cell.layer.masksToBounds = true

var label: UILabel!

if let lab: UILabel = cell.contentView.subviews.last as? UILabel{
label = lab
}else{
label = UILabel.init(frame: CGRect.init(x: 0, y: 0, width: 30, height: 30))
label.textAlignment = .center
cell.contentView.addSubview(label)
label.textColor = .white
}

label.text = "\(indexPath.row)"
return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 点击添加数据
self.dataArr += [300,200,300,400,100,200,300,400,100,200,300,400,100,200,300,400]
self.flowLayout.dataArr = self.dataArr
self.collectionView.reloadData()
}
}

核心代码

计算布局

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
let SCREEN_WIDTH = UIScreen.main.bounds.size.width
let SCREEN_HEIGHT = UIScreen.main.bounds.size.height

class WaterfallFlowLayout: UICollectionViewFlowLayout {
// 总列数
var columnCount:Int = 0
// 数据数组
var dataArr = [Int]()
// 整个collectionView的高度
private var maxH:Int?
//所有item的属性
fileprivate var layoutAttributesArray = [UICollectionViewLayoutAttributes]()

// 准备布局时调用
override func prepare() {
/**
* 计算每个item的宽度
* 即:(collectionView的宽度 - 左右边距和 - item的水平间距之和) / 每行的item数量
*/
let itemWidth = ((self.collectionView?.bounds.size.width)! - self.sectionInset.left - self.sectionInset.right - self.minimumInteritemSpacing * CGFloat(self.columnCount - 1)) / CGFloat.init(self.columnCount)

// 通过item的宽度,计算并设置属性
self.computeAttributesWithItemWidth(CGFloat(itemWidth))
}

///根据itemWidth计算布局属性
func computeAttributesWithItemWidth(_ itemWidth:CGFloat){

// 定义一个列高数组 记录每一列的总高度(初始值都为上边距)
var columnHeightArr = [Int](repeating: Int(self.sectionInset.top), count: self.columnCount)
// 定义一个 记录每一列的item个数的数组
var columnItemCountArr = [Int](repeating: 0, count: self.columnCount)

// 定义一个 存储属性的临时数组
var attributesArray = [UICollectionViewLayoutAttributes]()

// 遍历数据计算每个item的属性并布局
for (index, dj_height) in self.dataArr.enumerated() {

// 根据IndexPath获取Cell元素的属性
let attributes = UICollectionViewLayoutAttributes.init(forCellWith: IndexPath.init(item: index, section: 0))
// 找出最短列的下标
let minHeight:Int = columnHeightArr.sorted().first!
let column = columnHeightArr.firstIndex(of: minHeight)

// 将数据追加在最短列
columnItemCountArr[column!] += 1
// 计算该项的坐标
let itemX = (itemWidth + self.minimumInteritemSpacing) * CGFloat(column!) + self.sectionInset.left
let itemY = minHeight
// 计算item的高度(机型适配,注意比例缩放)
let itemH = dj_height
// 设置frame
attributes.frame = CGRect(x: itemX, y: CGFloat(itemY), width: itemWidth, height: CGFloat(itemH))

attributesArray.append(attributes)
// 累加列高
columnHeightArr[column!] += itemH + Int(self.minimumLineSpacing)
}

// 找出最高列的下标
let maxHeight:Int = columnHeightArr.sorted().last!
let maxHeightColumnIndex = columnHeightArr.firstIndex(of: maxHeight)
// 根据最高列设置itemSize的默认值 使用总高度的平均值
let itemH = (maxHeight - Int(self.minimumLineSpacing) * (columnItemCountArr[maxHeightColumnIndex!] + 1)) / columnItemCountArr[maxHeightColumnIndex!]
self.itemSize = CGSize(width: itemWidth, height: CGFloat(itemH))
// 给属性数组设置数值
self.layoutAttributesArray = attributesArray
// 将最高列的行高赋值给属性,作为contentSize.Height的值
self.maxH = maxHeight
}

// 返回计算好的layoutAttributesArray数组
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

return self.layoutAttributesArray
}

// 重写设置contentSize
override var collectionViewContentSize: CGSize {
get {
return CGSize(width: (collectionView?.bounds.width)!, height: CGFloat(self.maxH!))
}
set {
self.collectionViewContentSize = newValue
}
}

}

总结

本篇文章主要是对瀑布流的原理进行概述和提供思路,不涉及网络请求,动态内容计算等方面,应该还算是简单易懂的。感兴趣童鞋的可以Github上下载,欢迎交流学习。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信