Swift - UICollectionView 瀑布流

使用Swift實現(xiàn)簡單的瀑布流,OC的實現(xiàn)和Swift大同小異,主要還是了解一下實現(xiàn)思路,我這里做了三種效果:

  1. 豎向滑動,不帶Header和Footer;
  2. 豎向滑動,帶自定義Header和Footer;
  3. 橫向滑動,不帶Header和Footer;

效果如下:


豎向滑動(無Header和Footer).png
豎向滑動(有Header和Footer).png
橫向滑動(無Header和Footer).png

蘋果為我們提供的UICollectionView可以很方便的實現(xiàn)很多不同的布局效果,其中它的布局精髓在于UICollectionViewLayout類,所有的item布局在這里邊進行。因此我們可以自定義一個類來繼承UICollectionViewLayout,重寫UICollectionViewLayout類中的一些函數(shù)/屬性,來實現(xiàn)我們想要的瀑布流布局(自定義的這個類可參照UICollectionViewFlowLayout類)。

  • JYWaterfallFlowLayout 實現(xiàn)思路:
  1. 新建一個繼承UICollectionViewLayout類的文件JYWaterfallFlowLayout;
  2. 新建一個協(xié)議類JYWaterfallFlowLayoutProtocol,聲明一些函數(shù)用于變量設置的回調(diào)定義我們布局時需要的常量/變量等;
  3. 定義私有/公有的變量/常量等,并賦默認值;
  4. 重寫UICollectionViewLayout中的一些方法/變量,對item、header、footer進行布局計算等;
  /// 這里我們先了解一下需要重寫的函數(shù)/屬性的作用
  /// 準備布局,初始化一些信息和所有布局(在UICollectionView布局(或重新布局)時會調(diào))
  override func prepare() {} 
  /// 獲取 rect 范圍內(nèi)的所有 item 的布局,并返回計算好的布局結(jié)果
  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {}
  /// 自定義 item 的布局
  override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {}
  /// 自定義 header/footer 的布局
  override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {}
  /// 設置 collectionVIew 的 contentSize(滾動范圍)
  override var collectionViewContentSize: CGSize



下面就讓我們進入自定義布局重點代碼:

  1. 定義我們需要的協(xié)議方法

JYWaterfallFlowLayoutProtocol.swift 文件中
這里的協(xié)議函數(shù)是參照UICollectionViewFlowLayout定義的
注意:swift定義協(xié)議的可選函數(shù)時必須添加“@ objc”關(guān)鍵字

@objc protocol JYWaterfallFlowLayoutDelegate: NSObjectProtocol {
    // item 的 size (寬高轉(zhuǎn)換:WaterfallFlowVertical根據(jù)寬算高,WaterfallFlowHorizontal根據(jù)高算寬)
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, heightForItemAt indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, heightForItemAt indexPath: IndexPath, itemHeight: CGFloat) -> CGFloat
    
    // header/footer 的 size(僅限豎向滑動時使用)
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, referenceSizeForFooterInSection section:Int) -> CGSize
    
    // 每個 section 的內(nèi)邊距
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, insetForSectionAt section: Int) -> UIEdgeInsets
    // 每個 section 下顯示的 item 有多少列,返回每個 section 下的 item 的列數(shù)
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, columnNumberAt section:Int) -> Int
    // 每個 section 下顯示的 item 的最小行間距
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
    // 每個 section 下顯示的 item 的最小列間距
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
    // 本 section 的頭部和上個 section 的尾部的間距
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, spacingWithPreviousSectionForSectionAt section: Int) -> CGFloat
}


以下代碼在JYWaterfallFlowLayout.swift 文件中

  1. 定義一個瀑布流滑動方向的枚舉
/**
 *  枚舉:collectionView 滑動方向
 **/
enum UICollectionViewScrollDirection {
    case vertical    // 豎向滑動
    case horizontal  // 橫向滑動
}


  1. 定義需要的常量和變量
class JYWaterfallFlowLayout: UICollectionViewLayout {
    /**
     *  MARK: - 定義變量
     **/
    /// 屏幕的寬高
    fileprivate let screenWidth = UIScreen.main.bounds.size.width
    fileprivate let screenHeight = UIScreen.main.bounds.size.height
    public var NavigationHeight: CGFloat = 0
    public var iPhoneXBottomHeight: CGFloat = 0
    
    /// collectionView 相關(guān)
    // 記錄 collectionView 的 content 可滑動的范圍
    fileprivate var contentScope: CGFloat = 0
    // collectionView 的滑動方向,默認為豎向滑動
    public var scrollDirection: UICollectionViewScrollDirection = .vertical
    
    /// item 相關(guān)
    // item 的行/列數(shù),默認為2行/列
    public var lineCount: Int = 2
    public var interitemCount: Int = 2
    // item 之間的間距(行/列),默認間距為10
    public var lineSpacing: CGFloat = 10
    public var interitemSpacing: CGFloat = 10
    
    /// section 相關(guān)
    // section 的內(nèi)邊距,默認上下左右都為10
    public var sectionInset: UIEdgeInsets = UIEdgeInsets.zero
    // 是否要顯示 header/footer,默認不顯示
    public var isShowHeader: Bool = false
    public var isShowFooter: Bool = false
    // section 的 header/footer 的 size
    public var headerReferenceSize: CGSize = CGSize.zero
    public var footerReferenceSize: CGSize = CGSize.zero
    
    /// 數(shù)據(jù)處理相關(guān)
    // 存儲 item 的所有 layoutAttributes 數(shù)組
    fileprivate lazy var layoutAttributesArray: [UICollectionViewLayoutAttributes] = []
    // 存儲橫向滑動時 section 每一行的寬度/豎向滑動時 section 每一列的高度
    fileprivate lazy var lineWidthArray: [CGFloat] = []
    fileprivate lazy var interitemHeightArray: [CGFloat] = []
    
    /// 協(xié)議代理
    weak var delegate: JYWaterfallFlowLayoutDelegate?
}


  1. 重寫UICollectionViewLayout類中的一些函數(shù)/屬性

重寫prepare() 函數(shù)

/**
 *  MARK: - 重寫UICollectionViewLayout中的一些方法
 **/
extension JYWaterfallFlowLayout {
    /// 為自定義布局做些準備操作(在collectionView重新布局時,總會調(diào)用此方法)
    override func prepare() {
        super.prepare()
        
        /// 初始化數(shù)據(jù)
        // 清空數(shù)組中之前保存的所有布局數(shù)據(jù)
        layoutAttributesArray.removeAll()
        self.contentScope = 0.0

        // 設置代理
        if self.delegate == nil {
            self.delegate = self.collectionView?.delegate as? JYWaterfallFlowLayoutDelegate
        }
        
        /// 計算 section 下 item/header/footer 的布局
        setAllLayoutsForSection()
    }
}

自定義函數(shù),用于分解prepare()函數(shù)里的代碼

    /*
     *  設置 section 下的 item/header/footer 的布局,并緩存
     */
    fileprivate func setAllLayoutsForSection() {
        // 獲取 collectionView 中 section 的個數(shù)
        let getSectionCount = self.collectionView?.numberOfSections
        guard let sectionCount = getSectionCount else { return }
        
        // 遍歷 section 下的所有 item/header/footer,并計算出所有 item/header/footer 的布局
        for i in 0..<sectionCount {
            // 獲取 NSIndexPath
            let indexPath = NSIndexPath(index: i)
            // 這里獲取到的 IndexPath 是個數(shù)組,取其內(nèi)容要用 indexPath.first/indexPath[0],不能用 indexPath.section,否則會 crash
//            let indexPath = IndexPath(index: i)
            
            // 通過代理調(diào)用協(xié)議方法,更新一些變量的值
            invokeProxy(inSection: indexPath.section)
            
            // 設置 header 的布局,并緩存
            if isShowHeader {
                let headerAttributesArray = supplementaryViewAttributes(ofKind: UICollectionElementKindSectionHeader, indexPath: indexPath as IndexPath)
                self.layoutAttributesArray.append(contentsOf: headerAttributesArray)
            }
            
            // 清空數(shù)組中之前緩存的高度,留待使用(給下面的 item 計算 y 坐標時使用 -- 必須寫在header后面,否則會計算錯誤)
            interitemHeightArray.removeAll()
            lineWidthArray.removeAll()
            for _ in 0..<interitemCount {
                // 判斷是橫向滑動還是豎向滑動
                if scrollDirection == .horizontal {
                    // 緩存 collectionView 的 content 的寬度,為 item 的 x 坐標開始的位置
                    self.lineWidthArray.append(contentScope)
                } else {
                    // 緩存 collectionView 的 content 的高度,為 item 的 y 坐標開始的位置
                    self.interitemHeightArray.append(contentScope)
                }
            }
            // 設置 item 的布局,并緩存
            let itemAttributesArray = itemAttributes(inSection: indexPath.section)
            self.layoutAttributesArray.append(contentsOf: itemAttributesArray)
            
            // 設置 footer 的布局,并緩存
            if isShowFooter {
                let footerAttributesArray = supplementaryViewAttributes(ofKind: UICollectionElementKindSectionFooter, indexPath: indexPath as IndexPath)
                self.layoutAttributesArray.append(contentsOf: footerAttributesArray)
            }
        }
    }

調(diào)用代理方法重新給變量設置的新值(若已經(jīng)在外部實現(xiàn)了協(xié)議函數(shù)的前提下)

    /*
     *  調(diào)用代理方法
     */
    fileprivate func invokeProxy(inSection section: Int) {
        /// 返回 section 下 item 的列數(shù)
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:columnNumberAt:)))) ?? false) {
            self.interitemCount = (delegate?.collectionView!(self.collectionView!, layout: self, columnNumberAt: section)) ?? interitemCount
        }
        /// 返回 section 的內(nèi)邊距
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:insetForSectionAt:)))) ?? false) {
            self.sectionInset = (delegate?.collectionView!(self.collectionView!, layout: self, insetForSectionAt: section)) ?? sectionInset
        }
        /// 返回當前 section 的 header 與上個 section 的 footer 之間的間距
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:spacingWithPreviousSectionForSectionAt:)))) ?? false) {
            self.spacingWithPreviousSection = (delegate?.collectionView!(self.collectionView!, layout: self, spacingWithPreviousSectionForSectionAt: section)) ?? spacingWithPreviousSection
        }
        /// 返回 section 下的 item 之間的最小行間距
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:minimumLineSpacingForSectionAt:)))) ?? false) {
            self.lineSpacing = (delegate?.collectionView!(self.collectionView!, layout: self, minimumLineSpacingForSectionAt: section)) ?? lineSpacing
        }
        /// 返回 section 下的 item 之間的最小列間距
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:minimumInteritemSpacingForSectionAt:)))) ?? false) {
            self.interitemSpacing = (delegate?.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section)) ?? interitemSpacing
        }
    }

重寫布局item相關(guān)函數(shù)

    /// 獲取 rect 范圍內(nèi)的所有 item 的布局,并返回計算好的布局結(jié)果
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return layoutAttributesArray
    }
    
    /// 自定義 item 的布局
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 通過 indexPath,創(chuàng)建一個 UICollectionViewLayoutAttributes
        let layoutAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        
        // 判斷是豎向布局還是橫向布局
        if scrollDirection == .vertical {
            // 計算 item 的布局屬性
            self.verticalLayoutForItem(layoutAttributes, at: indexPath)
            
            /// 設置 collectionView 的 content 的高度
            //  獲取最大列的高度
            let (_, maximumInteritemHeight) = maximumInteritemForSection(heightArray: interitemHeightArray)
            //  判斷 collectionView 的 content 的高度 是否比當前計算的最大列的高度小,若小于則更新 collectionView 的 content 的值
            if contentScope < maximumInteritemHeight {
                self.contentScope = maximumInteritemHeight
                // collectionView的content的高度 + section底部內(nèi)邊距 + iphoneX的底部高度
                self.contentScope = contentScope + sectionInset.bottom + iPhoneXBottomHeight
            }
            
        } else if scrollDirection == .horizontal {
            // 計算 item 的布局屬性
            self.horizontalLayoutForItem(layoutAttributes, at: indexPath)
            
            /// 設置 collectionView 的 content 的寬度
            //  獲取最大列的高度
            let (_, maximumLineWidth) = maximumInteritemForSection(heightArray: lineWidthArray)  // maximumLineForSection(widthArray: lineWidthArray)
            //  判斷 collectionView 的 content 的寬度 是否比當前計算的最大行的寬度小,若小于則更新 collectionView 的 content 的值
            if contentScope < maximumLineWidth {
                self.contentScope = maximumLineWidth
                // collectionView的content的寬度 + section右側(cè)內(nèi)邊距
                self.contentScope = contentScope + sectionInset.right
            }
        }
        
        return layoutAttributes
    }

以下是計算item自定義函數(shù),分解layoutAttributesForItem函數(shù)的功能

    /*
     *  豎向布局:計算 item 的布局,并存儲每一列的高度
     *
     *  @param layoutAttributes: 布局屬性
     *  @param indexPath: 索引
     */
    fileprivate func verticalLayoutForItem(_ layoutAttributes: UICollectionViewLayoutAttributes, at indexPath: IndexPath) {
        /// 獲取 collectionView 的寬度
        let collectionViewWidth = self.collectionView?.frame.size.width ?? screenWidth
        
        /// 計算 item 的 frame
        //  item 的寬度【item的寬度 = (collectionView的寬度 - 左右內(nèi)邊距 - 列之間的間距 * (列數(shù) - 1)) / 列數(shù)】
        var itemWidth = (collectionViewWidth - sectionInset.left - sectionInset.right - interitemSpacing * CGFloat(interitemCount - 1)) / CGFloat(interitemCount)
        //  item 的高度【通過代理方法,并根據(jù) item 的寬度計算出 item 的高度】
        var itemHeight: CGFloat = 0
        if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:heightForItemAt:itemWidth:)))) ?? false) {
            itemHeight = delegate?.collectionView!(self.collectionView!, layout: self, heightForItemAt: indexPath, itemWidth: itemWidth) ?? 0
        }
        if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:sizeForItemAt:)))) ?? false) {
            let size = delegate?.collectionView!(self.collectionView!, layout: self, sizeForItemAt: indexPath) ?? CGSize.zero
            itemWidth = size.width
            itemHeight = size.height
        }
        //  獲取高度最小的那一列的列號和高度值
        let (minimumInteritemNumber, minimumInteritemHeight) = minimumInteritemForSection(heightArray: interitemHeightArray)
        //  item 的 x 坐標
        // 【x的坐標 = 左內(nèi)邊距 + 列號 * (item的寬度 + 列之間的間距)】
        let itemX = sectionInset.left + CGFloat(minimumInteritemNumber) * (itemWidth + interitemSpacing)
        //  item 的 y 坐標,初始位置為最小列的高度
        var itemY: CGFloat = minimumInteritemHeight
        //  如果item的y值不等于上個區(qū)域的最高的高度 既不是此區(qū)的第一列 要加上此區(qū)的每個item的上下間距
        if indexPath.item < interitemCount {
            itemY = itemY + sectionInset.top // y坐標值 + section內(nèi)邊距top值(也就是第一行上方是否留白)
        } else {
            itemY = itemY + lineSpacing      // y坐標值 + 行間距(item與item之間的行間距)
        }
        
        //  設置 item 的 attributes 的 frame
        layoutAttributes.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: itemHeight)
        
        /// 存儲所計算列的高度。若已存在,則更新其值;若不存在,則直接賦值(y值 + height)
        self.interitemHeightArray[minimumInteritemNumber] = layoutAttributes.frame.maxY
    }
    
    /*
     *  橫向布局:計算 item 的布局,并存儲每一行的寬度
     *
     *  @param layoutAttributes: 布局屬性
     *  @param indexPath: 索引
     */
    fileprivate func horizontalLayoutForItem(_ layoutAttributes: UICollectionViewLayoutAttributes, at indexPath: IndexPath) {
        /// 獲取 collectionView 的高度
        let collectionViewHeight = self.collectionView?.frame.size.height ?? (screenHeight - NavigationHeight)
        
        /// 計算 item 的 frame
        //  item 的高度【item的高度 = (collectionView的高度 - iphoneX底部的高度 - header的高度 - footer的高度 - 上下內(nèi)邊距 - 行之間的間距 * (行數(shù) - 1)) / 行數(shù)】
        var itemHeight = (collectionViewHeight - iPhoneXBottomHeight - headerReferenceSize.height - footerReferenceSize.height - sectionInset.top - sectionInset.bottom - lineSpacing * CGFloat(lineCount - 1)) / CGFloat(lineCount)
        //  item 的寬度【通過代理方法,并根據(jù) item 的高度計算出 item 的寬度】
        var itemWidth: CGFloat = 0
        if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:heightForItemAt:itemHeight:)))) ?? false) {
            itemWidth = delegate?.collectionView!(self.collectionView!, layout: self, heightForItemAt: indexPath, itemHeight: itemHeight) ?? 0
        }
        if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:sizeForItemAt:)))) ?? false) {
            let size = delegate?.collectionView!(self.collectionView!, layout: self, sizeForItemAt: indexPath) ?? CGSize.zero
            itemWidth = size.width
            itemHeight = size.height
        }
        //  獲取寬度最小的那一行的行號和寬度值
        let (minimumLineNumber, minimumLineWidth) = minimumInteritemForSection(heightArray: lineWidthArray)  // minimumLineForSection(widthArray: lineWidthArray)
        //  item 的 y 坐標
        // 【y的坐標 = 上內(nèi)邊距 + 行號 * (item的高度 + 行之間的間距)】
        let itemY = headerReferenceSize.height + sectionInset.top + CGFloat(minimumLineNumber) * (itemHeight + lineSpacing)
        //  item 的 x 坐標,初始位置為最小行的寬度
        var itemX: CGFloat = minimumLineWidth
        //  如果item的x值不等于上個區(qū)域的最高的高度 既不是此區(qū)的第一列 要加上此區(qū)的每個item的左右間距
        if indexPath.item < lineCount {
            itemX = itemX + sectionInset.left // x坐標值 + section內(nèi)邊距l(xiāng)eft值(也就是第一列左方是否留白)
        } else {
            itemX = itemX + interitemSpacing     // x坐標值 + 列間距(item與item之間的列間距)
        }
        
        //  設置 item 的 attributes 的 frame
        layoutAttributes.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: itemHeight)
        
        /// 存儲所計算列的高度(若已存在,則更新其值;若不存在,則直接賦值)
        self.lineWidthArray[minimumLineNumber] = layoutAttributes.frame.maxX
    }
    
    /*
     *  豎向布局: 計算高度最小的是哪一列                 / 橫向布局:計算寬度最小的是哪一行
     *
     *  @param  heightArray: 緩存 section 高度的數(shù)組  / 緩存 section 寬度的數(shù)組
     *  return  返回最小列的列號和高度值                / 返回最小行的行號和高度值
     */
    fileprivate func minimumInteritemForSection(heightArray: [CGFloat]) -> (Int, CGFloat) {
        if heightArray.count <= 0 {
            return (0, 0.0)
        }
        // 默認第0列的高度最小
        var minimumInteritemNumber = 0
        // 從緩存高度數(shù)組中取出第一個元素,作為最小的那一列的高度
        var minimumInteritemHeight = heightArray[0]
        // 遍歷數(shù)組,查找出最小的列號和最小列的高度值
        for i in 1..<heightArray.count {
            let tempMinimumInteritemHeight = heightArray[i]
            if minimumInteritemHeight > tempMinimumInteritemHeight {
                minimumInteritemHeight = tempMinimumInteritemHeight
                minimumInteritemNumber = i
            }
        }
        return (minimumInteritemNumber, minimumInteritemHeight)
    }
    
    /*
     *  豎向布局: 計算高度最大的是哪一列                 / 橫向布局:計算寬度最大的是哪一行
     *
     *  @param  heightArray: 緩存 section 高度的數(shù)組  / 緩存 section 寬度的數(shù)組
     *  return  返回最大列的列號和高度值                / 返回最大行的行號和寬度值
     */
    fileprivate func maximumInteritemForSection(heightArray: [CGFloat]) -> (Int, CGFloat) {
        if heightArray.count <= 0 {
            return (0, 0.0)
        }
        // 默認第0列的高度最小
        var maximumInteritemNumber = 0
        // 從緩存高度數(shù)組中取出第一個元素,作為最小的那一列的高度
        var maximumInteritemHeight = heightArray[0]
        // 遍歷數(shù)組,查找出最小的列號和最小列的高度值
        for i in 1..<heightArray.count {
            let tempMaximumInteritemHeight = heightArray[i]
            if maximumInteritemHeight < tempMaximumInteritemHeight {
                maximumInteritemHeight = tempMaximumInteritemHeight
                maximumInteritemNumber = i
            }
        }
        return (maximumInteritemNumber, maximumInteritemHeight)
    }

重寫布局Header和Footer的函數(shù)

    /// 自定義 header/footer 的布局
    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 通過 elementKind 和 indexPath,創(chuàng)建一個 UICollectionViewLayoutAttributes
        let layoutAttributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
        if elementKind == UICollectionElementKindSectionHeader {
            /// 通過代理方法,更新變量 headerReferenceSize 的值
            if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:referenceSizeForHeaderInSection:)))) ?? false) {
                self.headerReferenceSize = (delegate?.collectionView!(self.collectionView!, layout: self, referenceSizeForHeaderInSection: (indexPath.first ?? 0))) ?? headerReferenceSize
            }
            
            /// 計算 header 的布局
            self.layoutSupplementaryView(layoutAttributes, frame: CGRect(x: 0, y: contentScope, width: headerReferenceSize.width, height: headerReferenceSize.height), atTopWhiteSpace: false, atBottomWhiteSpace: false)
        } else if elementKind == UICollectionElementKindSectionFooter {
            /// 通過代理方法,更新變量 footerReferenceSize 的值
            if delegate != nil && ((delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:referenceSizeForFooterInSection:)))) ?? false) {
                self.footerReferenceSize = (delegate?.collectionView!(self.collectionView!, layout: self, referenceSizeForFooterInSection: (indexPath.first ?? 0))) ?? footerReferenceSize
            }
            
            /// 計算 footer 的布局
            self.layoutSupplementaryView(layoutAttributes, frame: CGRect(x: 0, y: contentScope, width: footerReferenceSize.width, height: footerReferenceSize.height), atTopWhiteSpace: false, atBottomWhiteSpace: false)
        } else {
            layoutAttributes.frame = CGRect.zero
        }
        return layoutAttributes
    }

以下是自定義函數(shù),分解layoutAttributesForSupplementaryView函數(shù)的功能

    /*
     *  豎向布局:計算 header/footer 布局,并更新 collectionView 的 content 的滾動范圍
     *
     *  @param layoutAttributes: 布局屬性
     *  @param frame           : header/footer 的frame
     *  @param top             : 是否 footer 的上方留白
     *  @param bottom          : 是否 header 的下方留白
     */
    fileprivate func layoutSupplementaryView(_ layoutAttributes: UICollectionViewLayoutAttributes, frame: CGRect, atTopWhiteSpace top: Bool, atBottomWhiteSpace bottom: Bool) {
        /// 計算 header/footer 的布局
        //  設置 header/footer 的 frame
        layoutAttributes.frame = frame
        
        ///  更新 collectionView 的 content 的值
        if isShowHeader {
            self.contentScope = self.contentScope + headerReferenceSize.height
        } else if isShowFooter {
            self.contentScope = self.contentScope + footerReferenceSize.height
        }
    }

重寫collectionViewContentSize屬性

    /// 設置 collectionVIew 的 content 的寬高(滾動范圍)
    override var collectionViewContentSize: CGSize {
        get {
            let _ = super.collectionViewContentSize
            if scrollDirection == .horizontal && lineWidthArray.count <= 0 {
                return CGSize.zero
            } else if scrollDirection == .vertical && interitemHeightArray.count <= 0 {
                return CGSize.zero
            }
            
            // 計算 collectionView 的 content 的 size
            let getCollectionViewWidth = self.collectionView?.frame.size.width
            let getCollectionViewHeight = self.collectionView?.frame.size.height
            // 記錄豎向滾動情況下 collectionView 固定的寬 / 橫向滾動情況下 collectionView 固定的高
            var collectionViewWidth: CGFloat = 0.0
            var collectionViewHeight: CGFloat = 0.0
            if let width = getCollectionViewWidth, let height = getCollectionViewHeight {
                collectionViewWidth = width
                collectionViewHeight = height
            } else {
                collectionViewWidth = screenWidth
                collectionViewHeight = screenHeight - NavigationHeight
            }
            // 記錄豎向滑動下的固定寬和動態(tài)高/橫向滑動下的動態(tài)寬和固定高
            let tempContentWidth = (scrollDirection == .vertical ? collectionViewWidth : contentScope)
            let tempContentHeight = (scrollDirection == .vertical ? contentScope : collectionViewHeight)
            return CGSize(width: tempContentWidth, height: tempContentHeight)
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,172評論 3 119
  • 給你寫一封情書 用七十年代的牛皮紙包住 字里行間不必用太多矯情的詞匯 也不用增加氣氛的排比句 沒有過多的修辭 只是...
    安陰閱讀 255評論 0 1
  • 那年是個四月,謝瓦里夫為反對有污染的工廠在巴圖姆鎮(zhèn)投建,與鎮(zhèn)上的人一起拒絕搬遷,被抓去關(guān)了兩個多月。 等到謝瓦里夫...
    俗眼閱讀 450評論 0 0

友情鏈接更多精彩內(nèi)容