如何通过纯代码创建自定义UICollectionViewFlowLayout
纯代码实现自定义CollectionViewLayout替代FlowLayout的方案
很简单,我们只需要替换布局的初始化方式,同时补全基类里未完成的代码逻辑就可以了,具体步骤如下:
1. 给自定义布局添加纯代码初始化器
原来的DiscoverCollection只有init?(coder:)(用于Storyboard加载),纯代码场景需要我们手动添加一个无参初始化方法:
import UIKit private let kReducedHeightColumnIndex = 1 private let kItemHeightAspect: CGFloat = 2 class DiscoverCollection: BaseCollectionViewLayout { private var _itemSize : CGSize! private var _columnsXoffset : [CGFloat]! // 新增纯代码初始化器 init() { super.init() self.totalColumns = 3 } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.totalColumns = 3 } private func isLastItemSingleInRow(_ indexPath: IndexPath) -> Bool { return indexPath.item == (totalItemsInSection - 1) && indexPath.item % totalColumns == 0 } override func calculateItemSize() { let contentWidthWithoutIndents = collectionView!.bounds.width - contentInsets.left - contentInsets.right let itemWidth = (contentWidthWithoutIndents - (CGFloat(totalColumns) - 1) * interItemsSpacing) / CGFloat(totalColumns) let itemHeight = itemWidth * kItemHeightAspect // 修复高度计算,匹配定义的高宽比 _itemSize = CGSize(width: itemWidth, height: itemHeight) _columnsXoffset = [] for columnIndex in 0..<totalColumns { // 加上左侧内边距,保证布局不溢出 _columnsXoffset.append(contentInsets.left + CGFloat(columnIndex) * (_itemSize.width + interItemsSpacing)) } } override func columnIndexForItemAt(indexPath: IndexPath) -> Int { let columnIndex = indexPath.item % totalColumns return self.isLastItemSingleInRow(indexPath) ? kReducedHeightColumnIndex : columnIndex } override func calculateItemFrame(indexPath: IndexPath, columnIndex: Int, columnYoffset: CGFloat) -> CGRect { let rowIndex = indexPath.item / totalColumns let halfItemHeight = (_itemSize.height - interItemsSpacing) / 2 var itemHeight = _itemSize.height if (rowIndex == 0 && columnIndex == kReducedHeightColumnIndex) || self.isLastItemSingleInRow(indexPath) { itemHeight = halfItemHeight } return CGRect(x: _columnsXoffset[columnIndex], y: columnYoffset, width: _itemSize.width, height: itemHeight) } }
2. 替换ViewController中的布局创建代码
把原来创建UICollectionViewFlowLayout的代码,换成初始化你的自定义DiscoverCollection:
/** Collection View **/ // 初始化自定义布局,替代FlowLayout let customLayout = DiscoverCollection() // 按需设置布局的自定义属性,比如item间距 customLayout.interItemsSpacing = 8 // 创建CollectionView时传入这个自定义布局 self.collectionView = UICollectionView( frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height), collectionViewLayout: customLayout ) collectionView.register(DiscoverCVC.self, forCellWithReuseIdentifier: cellId) collectionView.delegate = self collectionView.dataSource = self collectionView.backgroundColor = .clear self.addSubview(collectionView)
3. 补全BaseCollectionViewLayout的prepare方法(关键!)
你提供的基类代码中prepare()方法被截断了,这会导致布局完全无法工作,需要补全这个核心方法:
import UIKit class BaseCollectionViewLayout: UICollectionViewLayout { private var _layoutMap = [IndexPath : UICollectionViewLayoutAttributes]() private var _columnsYoffset : [CGFloat]! private var _contentSize : CGSize! private(set) var totalItemsInSection = 0 var totalColumns = 0 var interItemsSpacing : CGFloat = 8 var contentInsets : UIEdgeInsets { return collectionView!.contentInset } override var collectionViewContentSize: CGSize { return _contentSize } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var layoutAttributesArray = [UICollectionViewLayoutAttributes]() for(_, layoutAttributes) in _layoutMap { if rect.intersects(layoutAttributes.frame) { layoutAttributesArray.append(layoutAttributes) } } return layoutAttributesArray } func columnIndexForItemAt(indexPath: IndexPath) -> Int { return indexPath.item % totalColumns } func calculateItemFrame(indexPath: IndexPath, columnIndex: Int, columnYoffset: CGFloat) -> CGRect { return CGRect.zero } func calculateItemSize(){} override func prepare() { _layoutMap.removeAll() guard totalColumns > 0, let collectionView = collectionView else { return } // 初始化列的Y偏移,从顶部内边距开始 _columnsYoffset = Array(repeating: contentInsets.top, count: totalColumns) totalItemsInSection = collectionView.numberOfItems(inSection: 0) if totalItemsInSection > 0 { calculateItemSize() var contentSizeHeight: CGFloat = contentInsets.top for itemIndex in 0..<totalItemsInSection { let indexPath = IndexPath(item: itemIndex, section: 0) let columnIndex = columnIndexForItemAt(indexPath: indexPath) let columnYOffset = _columnsYoffset[columnIndex] // 计算当前cell的frame let itemFrame = calculateItemFrame(indexPath: indexPath, columnIndex: columnIndex, columnYoffset: columnYOffset) // 创建并保存布局属性 let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = itemFrame _layoutMap[indexPath] = attributes // 更新当前列的Y偏移(加上item间距) _columnsYoffset[columnIndex] = itemFrame.maxY + interItemsSpacing // 记录最大的Y值,作为内容高度的基准 contentSizeHeight = max(contentSizeHeight, itemFrame.maxY) } // 最终内容尺寸要加上底部内边距 _contentSize = CGSize( width: collectionView.bounds.width, height: contentSizeHeight + contentInsets.bottom ) } } }
这样一来,你的自定义布局就能完全替代FlowLayout,实现和Storyboard配置时一样的效果了。
内容的提问来源于stack exchange,提问作者AndrewS
相关产品推荐
相关产品推荐

