自定义UICollectionViewLayout(瀑布流)

1,032 阅读3分钟

起因

最近需要实现瀑布流的一个 UICollectionViewLayout, 网上东西一大堆,但是碍于自己对 Swift 了解不是太深, 还是想用 Swift 来实现,加深对 Swift 的理解

注意

实现自定义的UICollectionViewLayout 需要注意下面的几个方法

// 生成每个视图的布局属性(头尾视图和cell的布局属性)
override func prepare()
// 返回滚动区域的大小,当你的UICollectionView 不滚动的情况下可以检查这个方法
override var collectionViewContentSize: CGSize{}
// 返回该区域内的布局属性
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
 // 返回 indexpath 位置上的 cell 对应的布局属性
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

思路:

我们参照 UIKit 提供的 UICollectionViewFlowLayout, 使用 protocol 将 item 的 Size返回,这个事必须要实现的方法., 还有一些其他不是必须要实现的方法, 比如返回列数, 返回头尾视图的 Size, 行间距, 列间距等,

因为在 Swift 中默认 协议里的方法,都是必须要实现的, 我们可以使用 extension 来对可选实现的协议,进行默认的实现.

在每次布局的时候, 一定要注意记录最大的 底部距离,是 UICollectionView 的ContentSize,

还有在头尾视图上,对边距的处理

实现

具体的代码如下

import UIKit

// 瀑布流
protocol LYFWaterFlowLayoutDelegate:NSObjectProtocol{
    // require
    func waterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, sizeForItemAt indexPath:IndexPath) -> CGSize
    // optional
    // 头视图的Size
    func waterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, sizeForHeaderViewIn Section:Int) -> CGSize
    // 尾视图的Size
    func waterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, sizeForFooterViewIn Section:Int) -> CGSize
    // 列数
    func columnCountInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> Int
    // 列边距
    func columnMarginInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> CGFloat
    // 行边距
    func rowMarginInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> CGFloat
    // 边缘之间的距离
    func edgeInsetInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> UIEdgeInsets
    
}

// optional delagate 进行默认实现
extension LYFWaterFlowLayoutDelegate{
    func waterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout,sizeForHeaderViewIn Section:Int) -> CGSize{
        return CGSize.zero
    }
    
    func waterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, sizeForFooterViewIn Section:Int) -> CGSize{
        return CGSize.zero
    }
    func columnCountInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> Int{
        return 1
    }
    
    func columnMarginInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> CGFloat{
        return 0
    }
    
    func rowMarginInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> CGFloat{
        return 0
    }
    func edgeInsetInWaterFlowLayout(_ waterFlowLayout:LYFWaterFlowLayout, forItemAt IndexPath:IndexPath) -> UIEdgeInsets{
        return .zero
    }
}

class LYFWaterFlowLayout: UICollectionViewLayout {
    weak open var waterDelegate:LYFWaterFlowLayoutDelegate!
    /// 存放所有cell 的布局属性
    lazy var attrsArray:[UICollectionViewLayoutAttributes] = []
    /// 存放每一列的最大Y值
    lazy var columnHeights:[CGFloat] = []
    
    /// 存放每一行的最大X值
    lazy var rowWidths:[CGFloat] = []
    var maxColumnHeight:CGFloat = 0.0
    
    
    /// 初始化生成每个视图的布局属性
    override func prepare() {
        super.prepare()
        self.maxColumnHeight = 0.0
        self.columnHeights.removeAll()
        self.attrsArray.removeAll()
        
        if let collectionView = self.collectionView {
            let sectionCount = collectionView.numberOfSections
            for section in 0..<sectionCount {
                // head
                let headAttri = self.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath.init(item: 0, section: section))
                if let headAttri = headAttri {
                    self.attrsArray.append(headAttri)
                }
                // item
                let rowCount = collectionView.numberOfItems(inSection: section)
                for item in 0..<rowCount{
                    let indexPath = IndexPath.init(item: item, section: section)
                    let itemAttri = self.layoutAttributesForItem(at: indexPath)
                    if let itemAttri = itemAttri{
                        self.attrsArray.append(itemAttri)
                    }
                }
                // foot
                let footAttri = self.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: IndexPath.init(item: 0, section: section))
                if let footAttri = footAttri {
                    self.attrsArray.append(footAttri)
                }
                
            }
        
        }
        
    }
    
    override var collectionViewContentSize: CGSize{
        return CGSize(width: 0, height: self.maxColumnHeight)
    }
    
    
    // 返回一定区域内的所有的布局属性
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return self.attrsArray
    }
    
    // 返回 indexpath 位置上的 cell 对应的布局属性
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
        let width = self.collectionView?.frame.size.width
        
        let edgInsets = waterDelegate.edgeInsetInWaterFlowLayout(self, forItemAt: indexPath)
        let columCount = waterDelegate.columnCountInWaterFlowLayout(self, forItemAt: indexPath)
        let columMargin = waterDelegate.columnMarginInWaterFlowLayout(self, forItemAt: indexPath)
        let rowMargin = waterDelegate.rowMarginInWaterFlowLayout(self, forItemAt: indexPath)
        let temp = width! - edgInsets.left - edgInsets.right
        let marginWith = temp - CGFloat((columCount - 1)) * columMargin
        let itemWidth = marginWith / CGFloat(columCount)
        let itemHeight = waterDelegate.waterFlowLayout(self, sizeForItemAt: indexPath).height
        
        // 找到高度最短的那一列
        let min = minInArray(nums: self.columnHeights)
        let minY = min.1
        let minIndex = min.0
        let x = edgInsets.left + CGFloat(minIndex) * (itemWidth + columMargin)
        var y = minY
        
        if y != edgInsets.top {
            y = y + rowMargin
        }
        
        // 更新最短的那列的高度
        self.columnHeights[minIndex] = y + itemHeight
        // 记录内容的高度
        if self.maxColumnHeight < self.columnHeights[minIndex] {
            self.maxColumnHeight = self.columnHeights[minIndex]
        }
        
        attrs.frame = CGRect(x: x, y: y, width: itemWidth, height: itemHeight)
        
        return attrs
    }
    
    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        var attri:UICollectionViewLayoutAttributes!
        if elementKind == UICollectionView.elementKindSectionHeader {
            attri = UICollectionViewLayoutAttributes.init(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: indexPath)
            attri.frame = self.headerViewFrameOfVerticalWaterFlow(indexPath: indexPath)
        }else{
            attri = UICollectionViewLayoutAttributes.init(forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, with: indexPath)
            attri.frame = self.footerViewFrameOfVerticalWaterFlow(indexPath: indexPath)
        }
        return attri
    }
    
    
    
    /// 返回header 的 CGRect
    /// - Parameter indexPath: indexPath
    /// - Returns: header 的Frame
    func headerViewFrameOfVerticalWaterFlow(indexPath:IndexPath) -> CGRect {
        let edgInsets = waterDelegate.edgeInsetInWaterFlowLayout(self, forItemAt: indexPath)
        let columCount = waterDelegate.columnCountInWaterFlowLayout(self, forItemAt: indexPath)
        let rowMargin = waterDelegate.rowMarginInWaterFlowLayout(self, forItemAt: indexPath)
        let size = waterDelegate.waterFlowLayout(self, sizeForHeaderViewIn: indexPath.section)
        let x:CGFloat = 0.0
        var y = self.maxColumnHeight == 0.0 ? edgInsets.top : self.maxColumnHeight
        if self.waterDelegate.waterFlowLayout(self, sizeForFooterViewIn: indexPath.section).height == 0.0 {
            y = self.maxColumnHeight == 0.0 ? edgInsets.top : self.maxColumnHeight + rowMargin
        }
        
        self.maxColumnHeight = y + size.height
        self.columnHeights.removeAll()
        for index  in 0..<columCount{
            self.columnHeights.append(self.maxColumnHeight)
        }
        return CGRect(x: x, y: y, width: (self.collectionView?.frame.size.width)!, height: size.height)
    }
    
    
    
    /// footer 的 CGRect
    /// - Parameter indexPath: indexPath
    /// - Returns: footer 的 Frame
    func footerViewFrameOfVerticalWaterFlow(indexPath:IndexPath) -> CGRect {
        let edgInsets = waterDelegate.edgeInsetInWaterFlowLayout(self, forItemAt: indexPath)
        let columCount = waterDelegate.columnCountInWaterFlowLayout(self, forItemAt: indexPath)
        let rowMargin = waterDelegate.rowMarginInWaterFlowLayout(self, forItemAt: indexPath)
        let size = waterDelegate.waterFlowLayout(self, sizeForFooterViewIn: indexPath.section)
        let x:CGFloat = 0.0
        let y = self.maxColumnHeight == 0.0 ? edgInsets.top : self.maxColumnHeight + rowMargin
    
        self.maxColumnHeight = y + size.height
        self.columnHeights.removeAll()
        for index  in 0..<columCount{
            self.columnHeights.append(self.maxColumnHeight)
        }
        return CGRect(x: x, y: y, width: (self.collectionView?.frame.size.width)!, height: size.height)
    }
    
    
    
    /// 返回数组的最小值
    /// - Parameter nums: 数组
    /// - Returns: 最小值得下标 和 最小值
    func minInArray(nums:[CGFloat]) -> (Int,CGFloat) {
        var minIndex = 0
        var minValue = nums[0]
        for (index,value) in nums.enumerated() {
            if value < minValue {
                minValue = value
                minIndex = index
            }
        }
         return (minIndex,minValue)
    }
}

因工程繁杂, 先不放 demo 了