Swift - UICollectionView 瀑布流

使用Swift实现简单的瀑布流,OC的实现和Swift大同小异,主要还是了解一下实现思路,我这里做了三种效果:

  1. 竖向滑动,不带Header和Footer;
  2. 竖向滑动,带自定义Header和Footer;
  3. 横向滑动,不带Header和Footer;

效果如下:


竖向滑动(无Header和Footer).png
竖向滑动(有Header和Footer).png
横向滑动(无Header和Footer).png

苹果为我们提供的UICollectionView可以很方便的实现很多不同的布局效果,其中它的布局精髓在于UICollectionViewLayout类,所有的item布局在这里边进行。因此我们可以自定义一个类来继承UICollectionViewLayout,重写UICollectionViewLayout类中的一些函数/属性,来实现我们想要的瀑布流布局(自定义的这个类可参照UICollectionViewFlowLayout类)。

  • JYWaterfallFlowLayout 实现思路:
  1. 新建一个继承UICollectionViewLayout类的文件JYWaterfallFlowLayout;
  2. 新建一个协议类JYWaterfallFlowLayoutProtocol,声明一些函数用于变量设置的回调定义我们布局时需要的常量/变量等;
  3. 定义私有/公有的变量/常量等,并赋默认值;
  4. 重写UICollectionViewLayout中的一些方法/变量,对item、header、footer进行布局计算等;
  /// 这里我们先了解一下需要重写的函数/属性的作用
  /// 准备布局,初始化一些信息和所有布局(在UICollectionView布局(或重新布局)时会调)
  override func prepare() {} 
  /// 获取 rect 范围内的所有 item 的布局,并返回计算好的布局结果
  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. 定义我们需要的协议方法

JYWaterfallFlowLayoutProtocol.swift 文件中
这里的协议函数是参照UICollectionViewFlowLayout定义的
注意:swift定义协议的可选函数时必须添加“@ objc”关键字

@objc protocol JYWaterfallFlowLayoutDelegate: NSObjectProtocol {
    // item 的 size (宽高转换:WaterfallFlowVertical根据宽算高,WaterfallFlowHorizontal根据高算宽)
    @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 的内边距
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: JYWaterfallFlowLayout, insetForSectionAt section: Int) -> UIEdgeInsets
    // 每个 section 下显示的 item 有多少列,返回每个 section 下的 item 的列数
    @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 相关
    // 记录 collectionView 的 content 可滑动的范围
    fileprivate var contentScope: CGFloat = 0
    // collectionView 的滑动方向,默认为竖向滑动
    public var scrollDirection: UICollectionViewScrollDirection = .vertical
    
    /// item 相关
    // item 的行/列数,默认为2行/列
    public var lineCount: Int = 2
    public var interitemCount: Int = 2
    // item 之间的间距(行/列),默认间距为10
    public var lineSpacing: CGFloat = 10
    public var interitemSpacing: CGFloat = 10
    
    /// section 相关
    // section 的内边距,默认上下左右都为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
    
    /// 数据处理相关
    // 存储 item 的所有 layoutAttributes 数组
    fileprivate lazy var layoutAttributesArray: [UICollectionViewLayoutAttributes] = []
    // 存储横向滑动时 section 每一行的宽度/竖向滑动时 section 每一列的高度
    fileprivate lazy var lineWidthArray: [CGFloat] = []
    fileprivate lazy var interitemHeightArray: [CGFloat] = []
    
    /// 协议代理
    weak var delegate: JYWaterfallFlowLayoutDelegate?
}


  1. 重写UICollectionViewLayout类中的一些函数/属性

重写prepare() 函数

/**
 *  MARK: - 重写UICollectionViewLayout中的一些方法
 **/
extension JYWaterfallFlowLayout {
    /// 为自定义布局做些准备操作(在collectionView重新布局时,总会调用此方法)
    override func prepare() {
        super.prepare()
        
        /// 初始化数据
        // 清空数组中之前保存的所有布局数据
        layoutAttributesArray.removeAll()
        self.contentScope = 0.0

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

自定义函数,用于分解prepare()函数里的代码

    /*
     *  设置 section 下的 item/header/footer 的布局,并缓存
     */
    fileprivate func setAllLayoutsForSection() {
        // 获取 collectionView 中 section 的个数
        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 是个数组,取其内容要用 indexPath.first/indexPath[0],不能用 indexPath.section,否则会 crash
//            let indexPath = IndexPath(index: i)
            
            // 通过代理调用协议方法,更新一些变量的值
            invokeProxy(inSection: indexPath.section)
            
            // 设置 header 的布局,并缓存
            if isShowHeader {
                let headerAttributesArray = supplementaryViewAttributes(ofKind: UICollectionElementKindSectionHeader, indexPath: indexPath as IndexPath)
                self.layoutAttributesArray.append(contentsOf: headerAttributesArray)
            }
            
            // 清空数组中之前缓存的高度,留待使用(给下面的 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)
            }
        }
    }

调用代理方法重新给变量设置的新值(若已经在外部实现了协议函数的前提下)

    /*
     *  调用代理方法
     */
    fileprivate func invokeProxy(inSection section: Int) {
        /// 返回 section 下 item 的列数
        if (delegate != nil && (delegate?.responds(to: #selector(JYWaterfallFlowLayoutDelegate.collectionView(_:layout:columnNumberAt:)))) ?? false) {
            self.interitemCount = (delegate?.collectionView!(self.collectionView!, layout: self, columnNumberAt: section)) ?? interitemCount
        }
        /// 返回 section 的内边距
        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相关函数

    /// 获取 rect 范围内的所有 item 的布局,并返回计算好的布局结果
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return layoutAttributesArray
    }
    
    /// 自定义 item 的布局
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 通过 indexPath,创建一个 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底部内边距 + 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右侧内边距
                self.contentScope = contentScope + sectionInset.right
            }
        }
        
        return layoutAttributes
    }

以下是计算item自定义函数,分解layoutAttributesForItem函数的功能

    /*
     *  竖向布局:计算 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的宽度 - 左右内边距 - 列之间的间距 * (列数 - 1)) / 列数】
        var itemWidth = (collectionViewWidth - sectionInset.left - sectionInset.right - interitemSpacing * CGFloat(interitemCount - 1)) / CGFloat(interitemCount)
        //  item 的高度【通过代理方法,并根据 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的坐标 = 左内边距 + 列号 * (item的宽度 + 列之间的间距)】
        let itemX = sectionInset.left + CGFloat(minimumInteritemNumber) * (itemWidth + interitemSpacing)
        //  item 的 y 坐标,初始位置为最小列的高度
        var itemY: CGFloat = minimumInteritemHeight
        //  如果item的y值不等于上个区域的最高的高度 既不是此区的第一列 要加上此区的每个item的上下间距
        if indexPath.item < interitemCount {
            itemY = itemY + sectionInset.top // y坐标值 + section内边距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的高度 - 上下内边距 - 行之间的间距 * (行数 - 1)) / 行数】
        var itemHeight = (collectionViewHeight - iPhoneXBottomHeight - headerReferenceSize.height - footerReferenceSize.height - sectionInset.top - sectionInset.bottom - lineSpacing * CGFloat(lineCount - 1)) / CGFloat(lineCount)
        //  item 的宽度【通过代理方法,并根据 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的坐标 = 上内边距 + 行号 * (item的高度 + 行之间的间距)】
        let itemY = headerReferenceSize.height + sectionInset.top + CGFloat(minimumLineNumber) * (itemHeight + lineSpacing)
        //  item 的 x 坐标,初始位置为最小行的宽度
        var itemX: CGFloat = minimumLineWidth
        //  如果item的x值不等于上个区域的最高的高度 既不是此区的第一列 要加上此区的每个item的左右间距
        if indexPath.item < lineCount {
            itemX = itemX + sectionInset.left // x坐标值 + section内边距left值(也就是第一列左方是否留白)
        } 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 高度的数组  / 缓存 section 宽度的数组
     *  return  返回最小列的列号和高度值                / 返回最小行的行号和高度值
     */
    fileprivate func minimumInteritemForSection(heightArray: [CGFloat]) -> (Int, CGFloat) {
        if heightArray.count <= 0 {
            return (0, 0.0)
        }
        // 默认第0列的高度最小
        var minimumInteritemNumber = 0
        // 从缓存高度数组中取出第一个元素,作为最小的那一列的高度
        var minimumInteritemHeight = heightArray[0]
        // 遍历数组,查找出最小的列号和最小列的高度值
        for i in 1..<heightArray.count {
            let tempMinimumInteritemHeight = heightArray[i]
            if minimumInteritemHeight > tempMinimumInteritemHeight {
                minimumInteritemHeight = tempMinimumInteritemHeight
                minimumInteritemNumber = i
            }
        }
        return (minimumInteritemNumber, minimumInteritemHeight)
    }
    
    /*
     *  竖向布局: 计算高度最大的是哪一列                 / 横向布局:计算宽度最大的是哪一行
     *
     *  @param  heightArray: 缓存 section 高度的数组  / 缓存 section 宽度的数组
     *  return  返回最大列的列号和高度值                / 返回最大行的行号和宽度值
     */
    fileprivate func maximumInteritemForSection(heightArray: [CGFloat]) -> (Int, CGFloat) {
        if heightArray.count <= 0 {
            return (0, 0.0)
        }
        // 默认第0列的高度最小
        var maximumInteritemNumber = 0
        // 从缓存高度数组中取出第一个元素,作为最小的那一列的高度
        var maximumInteritemHeight = heightArray[0]
        // 遍历数组,查找出最小的列号和最小列的高度值
        for i in 1..<heightArray.count {
            let tempMaximumInteritemHeight = heightArray[i]
            if maximumInteritemHeight < tempMaximumInteritemHeight {
                maximumInteritemHeight = tempMaximumInteritemHeight
                maximumInteritemNumber = i
            }
        }
        return (maximumInteritemNumber, maximumInteritemHeight)
    }

重写布局Header和Footer的函数

    /// 自定义 header/footer 的布局
    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 通过 elementKind 和 indexPath,创建一个 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
    }

以下是自定义函数,分解layoutAttributesForSupplementaryView函数的功能

    /*
     *  竖向布局:计算 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
            }
            // 记录竖向滑动下的固定宽和动态高/横向滑动下的动态宽和固定高
            let tempContentWidth = (scrollDirection == .vertical ? collectionViewWidth : contentScope)
            let tempContentHeight = (scrollDirection == .vertical ? contentScope : collectionViewHeight)
            return CGSize(width: tempContentWidth, height: tempContentHeight)
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,982评论 3 119
  • 给你写一封情书 用七十年代的牛皮纸包住 字里行间不必用太多矫情的词汇 也不用增加气氛的排比句 没有过多的修辞 只是...
    安阴阅读 214评论 0 1
  • 那年是个四月,谢瓦里夫为反对有污染的工厂在巴图姆镇投建,与镇上的人一起拒绝搬迁,被抓去关了两个多月。 等到谢瓦里夫...
    俗眼阅读 413评论 0 0