UICollectionView 自定义布局实现瀑布流视图

网友投稿 652 2022-09-05

UICollectionView 自定义布局实现瀑布流视图

UICollectionView 自定义布局实现瀑布流视图

自打 Apple 在 iOS6 中引入 UICollectionView 这个控件之后,越来越多的 iOS 开发者选择将它作为构建 UI 的首选,如此吸引人的原因在于它的可定制化程度很高,非常的灵活,这取决于它有一个单独的对象来管理布局,布局决定了视图的位置和属性。

说到布局 layout,大家在开发过程中与 UICollectionView 搭配使用最多的 应该就是 UICollectionViewFlowLayout 了,这是 UIKit 提供给开发者最基础的的网格布局,如果我们稍微要求高一点的定制化布局需求,它就没法满足实际的要求了,我们能否实现自定义的布局方案呢!答案当然是可以的。

在今天的这篇文章中,我将演示如何实现一个自定义的瀑布流布局方案,类似下图:

大家在这个过程中会学习到以下几个知识点:

关于自定义布局动态尺寸 Cell 的处理计算和缓存布局属性

好了,废话不多说,咱就开始吧!

自定义布局

日常开发中,我们使用 UICollectionView 控件都会搭配一个默认的,提供一些基础的布局 UICollectionViewFlowLayout 来使用,但是当我们需要实现定制化程度比较高的界面时,就得自己实现一个自定义布局了。

那么,我们该如何来实现一个自定义布局呢!

查阅苹果的文档可以得知,UICollectionView 的布局是抽象类 UICollectionViewLayout 的子类,它定义了 UICollectionView 中每个 Item 的布局属性叫做:UICollectionViewLayoutAttributes,所以我们可以通过继承 UICollectionViewLayout,然后对每个 item 的 UICollectionViewLayoutAttributes 做调整,例如它的尺寸,旋转角度,缩放等等。

既然 Apple 的开发文档已经说得很明白了,那么我们就可以先完成这些基础的工作:

动态尺寸

有的人会问,瀑布流视图的惊艳之处就在于它的每个 Cell 的尺寸都是不一致的,那如何生成动态高度的 Cell 呢!

这里我用了 Swift 生成随机数的方式,在给每个 item 设置 frame 的时候,随机生成一个高度,这也是我们创建动态化界面的常用方式,这个代码逻辑就比较简单了,一行代码即可搞定:

CGFloat(arc4random_uniform(150) + 50)

计算和缓存布局属性

在实现该功能之前,我们先了解一下 UICollectionView 的布局过程,它与布局对象之间的关系是一种协作的关系,当 UICollectionView 需要一些布局信息的时候,它会去调用布局对象的一些函数,这些函数的执行是有一定的次序的,如图所示:

所以我们继承自 UICollectionViewLayout 的子类必须要实现以下方法:

1. override var collectionViewContentSize: CGSize {...}

This method returns the width and height of the collection view’s contents. You must implement it to return the height and width of the entire collection view’s content, not just the visible content. The collection view uses this information internally to configure its scroll view’s content size.

2. override func prepare()

Whenever a layout operation is about to take place, UIKit calls this method. It’s your opportunity to prepare and perform any calculations required to determine the collection view’s size and the positions of the items.

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

In this method, you return the layout attributes for all items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.

4. override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {...}

This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.

了解完需要实现的函数后,接下来就开始计算瀑布流视图的布局属性了,在这里我先讲一下我实现的大概思路吧!

Cell 高度动态化

protocol WaterFallLayoutDelegate: NSObjectProtocol { func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat

属性计算

Cell 高度动态化已经解决,那如何能让每个 Cell 都能紧密的挨在一起呢!这里我的策略就是通过追踪计算每一列的高度值来得出最小高度的那一列,由于已知当前有最小高度的那一列的高度值以及索引值,那我们就可以为一个 Cell 计算得出它新的 X 坐标 和 Y 坐标,然后重新对该 Cell 的位置信息赋值,最后再更新一下每列的高度,直到为每一个 Cell 都重新计算了一遍它的位置。

我们可以在 prepare() 函数中,添加这些逻辑,代码如下:

override func prepare() { super.prepare() // 计算每个 Cell 的宽度 let itemWidth = (collectionView!.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols) // Cell 数量 let itemCount = collectionView!.numberOfItems(inSection: 0) // 最小高度索引 var minHeightIndex = 0 // 遍历 item 计算并缓存属性 for i in layoutAttributeArray.count ..< itemCount { let indexPath = IndexPath(item: i, section: 0) let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath) // 获取动态高度 let itemHeight = delegate?.waterFlowLayout(self, itemHeight: indexPath) // 找到高度最短的那一列 let value = yArray.min() // 获取数组索引 minHeightIndex = yArray.firstIndex(of: value!)! // 获取该列的 Y 坐标 var itemY = yArray[minHeightIndex] // 判断是否是第一行,如果换行需要加上行间距 if i >= cols { itemY += minimumInteritemSpacing } // 计算该索引的 X 坐标 let itemX = sectionInset.left + (itemWidth + minimumInteritemSpacing) * CGFloat(minHeightIndex) // 赋值新的位置信息 attr.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: CGFloat(itemHeight!)) // 缓存布局属性 layoutAttributeArray.append(attr) // 更新最短高度列的数据 yArray[minHeightIndex] = attr.frame.maxY } maxHeight = yArray.max()! + sectionInset.bottom

接下来,在 layoutAttributesForElements(in rect: CGRect) 方法中添加如下逻辑:

这个方法决定了哪些 item 在给定的区域内是可见的,我们可以通过数组提供的过滤的方法 filter ,检查之前计算的布局属性是否与该可见区域相交,然后并把相交的属性返回,代码如下:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributeArray.filter { $0.frame.intersects(rect) }}

好了,到这里关于瀑布流视图的布局就讲完了,附上 WaterFallFlowLayout 的全部代码,供大家参考:

import UIKitprotocol WaterFallLayoutDelegate: NSObjectProtocol { func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat}class WaterFallFlowLayout: UICollectionViewFlowLayout { weak var delegate: WaterFallLayoutDelegate? // 列数 var cols = 4 // 布局数组 fileprivate lazy var layoutAttributeArray: [UICollectionViewLayoutAttributes] = [] // 高度数组 fileprivate lazy var yArray: [CGFloat] = Array(repeating: self.sectionInset-, count: cols) fileprivate var maxHeight: CGFloat = 0 override func prepare() { super.prepare() // 计算每个 Cell 的宽度 let itemWidth = (collectionView!.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols) // Cell 数量 let itemCount = collectionView!.numberOfItems(inSection: 0) // 最小高度索引 var minHeightIndex = 0 // 遍历 item 计算并缓存属性 for i in layoutAttributeArray.count ..< itemCount { let indexPath = IndexPath(item: i, section: 0) let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath) // 获取动态高度 let itemHeight = delegate?.waterFlowLayout(self, itemHeight: indexPath) // 找到高度最短的那一列 let value = yArray.min() // 获取数组索引 minHeightIndex = yArray.firstIndex(of: value!)! // 获取该列的 Y 坐标 var itemY = yArray[minHeightIndex] // 判断是否是第一行,如果换行需要加上行间距 if i >= cols { itemY += minimumInteritemSpacing } // 计算该索引的 X 坐标 let itemX = sectionInset.left + (itemWidth + minimumInteritemSpacing) * CGFloat(minHeightIndex) // 赋值新的位置信息 attr.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: CGFloat(itemHeight!)) // 缓存布局属性 layoutAttributeArray.append(attr) // 更新最短高度列的数据 yArray[minHeightIndex] = attr.frame.maxY } maxHeight = yArray.max()! + sectionInset.bottom }}extension WaterFallFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributeArray.filter { $0.frame.intersects(rect) } } override var collectionViewContentSize: CGSize { return CGSize(width: collectionView!.bounds.width, height: maxHeight) }}

在 UIViewController 中呈现

完成上述的瀑布流布局后,那是时候应该在 UIViewController 中将它呈现出来了,接下来的步骤就比较简单了,相信大家都能够独自完成,我就不做详细的解释了,附上代码:

import UIKitclass WaterFallViewController: UIViewController { private let cellID = "baseCellID" var itemCount: Int = 30 var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. setUpView() } func setUpView() { // 设置 flowlayout let layout = WaterFallFlowLayout() layout.delegate = self // 设置 collectionview let margin: CGFloat = 8 layout.minimumLineSpacing = margin layout.minimumInteritemSpacing = margin layout.sectionInset = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.backgroundColor = .white collectionView.dataSource = self // 注册 Cell collectionView.register(BaseCollectionViewCell.self, forCellWithReuseIdentifier: cellID) view.addSubview(collectionView) }}extension WaterFallViewController: UICollectionViewDelegate{}extension WaterFallViewController: UICollectionViewDataSource{ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return itemCount } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! BaseCollectionViewCell cell.cellIndex = indexPath.item cell.backgroundColor = indexPath.item % 2 == 0 ? .systemBlue : .purple if itemCount - 1 == indexPath.item { itemCount += 20 collectionView.reloadData() } return cell }}extension WaterFallViewController: WaterFallLayoutDelegate{ func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat { return CGFloat(arc4random_uniform(150) + 50) }}

将上述代码添加到 Xcode 工程中编译并运行,你就会看到 Cell 根据照片的高度正确放置并设置了大小:

好了, 利用 UICollectionView 控件与自定义布局实现瀑布流的内容到此就结束了,最后附上项目的源码地址:

​​github.com/ShenJieSuzh…​​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:11gr2 check status of resources in cluster
下一篇:用 Node + MySQL 处理 100G 数据(用我的手指扰乱吧.∼在打烊后仅剩两人免费观看樱花)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~