[iOS笔记]UICollectionView实现瀑布流

写在前面

在iOS开发中UITableView随处可见,但他的整体布局并没有像UICollectionView一样可以自定义布局。通过自定义UICollectionView的布局,我们灵活的做出个很多不一样的布局。在这一篇笔记中,可以学到的是如何自己实现一个普通的瀑布流布局。

瀑布流第一版.gif

普通使用

这里介绍的是使用Xib在正常使用UICollectionView下的操作

let layout = WaterfallLayout()
layout.itemSize = { [unowned self] indexPath in
    let model = self.commodities[indexPath.item]
    return CGSize(width: model.w, height: model.h)
}
layout.headerHeight = { _ in return 20 }
layout.footerHeight = { _ in return 30 }
let collectionView = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: layout)

其他UICollectionViewDatasource的步骤就不重复了

主要思路

UICollectionViewUITableView最大的不同是,前者在初始化的时候需要传入一个UICollectionViewLayout对象。在默认的UICollectionView中有一个默认的LayoutUICollectionViewFlowLayout即是流式布局,就是平时最常见的类似“九宫格”的布局。所以,要做到瀑布流布局,自定义一个UICollectionViewLayout是必须的了,在里面我们计算itemframe

重要步骤

重写下面的方法

/// 布局准备
- (void)prepareLayout;
/// 获取属性数组
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
/// 根据 indexPath 返回属性
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
/// 计算ContentSize
- (CGSize)collectionViewContentSize;

实现步骤

  1. 声明WaterfallLayout(瀑布流布局)继承自UICollectionViewLayout
class WaterfallLayout: UICollectionViewLayout {
    ...
}
  1. 使用闭包来为瀑布流提供itemSize
var itemSize: ((IndexPath) -> CGSize) = { _ in
    return .zero
}
  1. 设置一个属性保存计算好的属性
/// 缓存布局属性数组
private var attributesArray = [UICollectionViewLayoutAttributes]()
  1. 重写prepare()方法,注意需要先super.prepare()
override func prepare() {
    super.prepare()
    guard let collectionView = collectionView else {
        return
    }
    // 清除缓存
    flowHeights = Array(repeating: edgeInsets.top, count: flowCount)
    attributesArray.removeAll()
    // 根据风格处缓存用于计算的定值
    prepareValueForCompute()
    
    // 创建新的布局属性
    for section in 0..
  1. 重写func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return attributesArray
}
  1. 重写在prepare()中调用的下列方法,主要是计算UICollectionViewLayoutAttributes中的frame
/// 返回头尾视图布局属性
func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

/// 返回每个 indexPath 对应的 item 的布局属性
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
  1. 我的返回每个 indexPath 对应的 item 的布局属性实现,纯属参考,主要的瀑布流计算量都在这里面。
/// 计算垂直瀑布的Frame
func verticalItemFrame(with indexPath: IndexPath) -> CGRect {
    // 布局的宽度和高度
    let width = flowWidth
    let size = itemSize(indexPath)
    let aspectRatio = size.height / size.width
    let height = width * aspectRatio
    
    // 查找最短的一列索引和值
    var destColumn = 0
    var minColumnHeight = flowHeights[0]
    for (i, v) in flowHeights.enumerated() {
        if v < minColumnHeight {
            minColumnHeight = v
            destColumn = i
        }
    }
    
    // 布局的坐标点
    let x = edgeInsets.left + CGFloat(destColumn) * (width + columnMargin)
    var y = minColumnHeight
    if y != edgeInsets.top {
        y += rowMargin
    }
    
    let rect = CGRect(x: x, y: y, width: width, height: height)
    
    // 更新最短那列的高度
    flowHeights[destColumn] = rect.maxY
    
    return rect
}
  1. 如果需要显示Header和Footer需要重写下面的方法,具体见下面的Demo
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
  1. 计算collectionViewContentSize
override var collectionViewContentSize: CGSize {
        switch style {
        case .vertical:
            ...
        case .horizontal:
            ...
        }
    }

未完待续

上面的展示的是我计算普通垂直瀑布流的大致方法,大家有什么好的方式和我做的不足的地方都可以说说

demo地址

你可能感兴趣的:([iOS笔记]UICollectionView实现瀑布流)