如何实现不等高UICollectionView Item的居中布局?
解决UICollectionView瀑布流Item居中及消除上下空白问题
我帮你调整了DynamicLayout的代码,既保留了Pinterest瀑布流的核心逻辑,又实现了布局水平居中、消除Item上下不必要空白的效果。下面是修改后的完整代码,以及关键调整点的说明:
修改后的完整代码
class DynamicLayout: UICollectionViewFlowLayout { weak var delegate: DynamicLayoutDelegate? // 改为weak避免循环引用 var numberOfColumns = 2 fileprivate var cellHorizontalPadding: CGFloat = 6 // 仅保留水平方向间距 fileprivate var contentHeight: CGFloat = 0 fileprivate var contentWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.bounds.width - (collectionView.contentInset.left + collectionView.contentInset.right) } fileprivate var cache = [UICollectionViewLayoutAttributes]() override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } override func prepare() { guard cache.isEmpty, let collectionView = collectionView else { return } // 1. 计算列宽与水平偏移,确保整体布局居中 let totalHorizontalSpacing = cellHorizontalPadding * CGFloat(numberOfColumns - 1) let columnWidth = (contentWidth - totalHorizontalSpacing) / CGFloat(numberOfColumns) // 如果需要固定列宽为310,替换上面两行: // let columnWidth: CGFloat = 310 // let totalHorizontalSpacing = cellHorizontalPadding * CGFloat(numberOfColumns - 1) // let totalColumnsWidth = columnWidth * CGFloat(numberOfColumns) + totalHorizontalSpacing // let leftInset = (contentWidth - totalColumnsWidth) / 2 var xOffset = [CGFloat]() for column in 0..<numberOfColumns { // 若用固定列宽,改为:xOffset.append(leftInset + CGFloat(column) * (columnWidth + cellHorizontalPadding)) xOffset.append(CGFloat(column) * (columnWidth + cellHorizontalPadding)) } var yOffset = [CGFloat](repeating: 0, count: numberOfColumns) var column = 0 // 初始化列索引 // 2. 遍历所有Item计算布局属性 for item in 0..<collectionView.numberOfItems(inSection: 0) { let indexPath = IndexPath(item: item, section: 0) guard let cellHeight = delegate?.collectionView(collectionView, heightFor: indexPath) else { continue } // 3. 消除上下空白:垂直方向无额外间距,Item直接衔接 let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: cellHeight) // 若需要保留极小垂直间距,取消下面注释并调整cellPadding值: // let cellVerticalPadding: CGFloat = 2 // let height = cellVerticalPadding * 2 + cellHeight // let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height) // let insetFrame = frame.insetBy(dx: cellHorizontalPadding, dy: cellVerticalPadding) // 4. 创建并缓存布局属性 let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = frame // 若保留垂直间距则改为insetFrame cache.append(attributes) // 5. 更新列的垂直偏移和整体内容高度 contentHeight = max(contentHeight, frame.maxY) yOffset[column] = frame.maxY // 无额外间距,直接衔接下一个Item // 切换到下一列 column = column < (numberOfColumns - 1) ? (column + 1) : 0 } } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]() for attributes in cache { if attributes.frame.intersects(rect) { visibleLayoutAttributes.append(attributes) } } return visibleLayoutAttributes } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return cache[indexPath.item] } } protocol DynamicLayoutDelegate: class { func collectionView(_ collectionView: UICollectionView, heightFor indexPath: IndexPath) -> CGFloat }
关键调整点说明
解决布局水平居中问题:
- 自动计算列宽(或固定列宽时计算左右内边距),让所有列整体居中显示在CollectionView内,消除左右侧的空白。
- 优化
xOffset计算逻辑,保证列与列之间的水平间距均匀一致。
消除Item上下空白:
- 将原有的通用
cellPadding拆分为仅作用于水平方向的cellHorizontalPadding,垂直方向直接让Item上下衔接,彻底消除不必要的空白。 - 若需要保留极小的垂直间距,可取消代码中相关注释并调整垂直padding值。
- 将原有的通用
修复内存泄漏风险:
- 将
delegate改为weak引用,避免CollectionView与布局之间形成循环引用导致内存泄漏。
- 将
增强代码健壮性:
- 增加
guard let处理cellHeight的可选值,避免因代理方法未实现导致的崩溃。 - 移除了原代码中冗余的
xOffset变量定义,避免变量冲突。
- 增加
如果你的需求是让同一行的不同列Item垂直居中对齐(而非瀑布流的顶部对齐),可以告诉我,我再帮你调整布局逻辑~
内容的提问来源于stack exchange,提问作者thatmarcel




