Swift4中如何让多分区CollectionView的行水平居中?
解决Swift 4多分区CollectionView行水平居中的问题
我太懂这种挫败感了——试过KTCenterFlowLayout却在多分区场景下掉链子,确实很多第三方布局库对多分区的适配都没做周全。不过咱们完全可以自己撸一个自定义UICollectionViewFlowLayout,完美适配多分区的行居中需求,步骤也不复杂:
第一步:创建自定义居中布局类
新建一个继承自UICollectionViewFlowLayout的类,重写布局属性计算方法,对每一行的item进行居中调整:
class CenteredCollectionViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // 获取系统默认计算的布局属性并复制一份,避免修改原数据 guard let originalAttributes = super.layoutAttributesForElements(in: rect) else { return nil } let attributesArray = NSArray(array: originalAttributes, copyItems: true) as! [UICollectionViewLayoutAttributes] // 按「分区+行」分组,把同一分区同一行的cell归为一组 var rowGroups = [String: [UICollectionViewLayoutAttributes]]() for attributes in attributesArray { guard attributes.representedElementCategory == .cell else { continue } // 用分区号和行的y坐标哈希值作为组的key,确保同一行的cell分到一组 let section = attributes.indexPath.section let rowY = attributes.frame.origin.y let groupKey = "\(section)-\(rowY.hashValue)" rowGroups[groupKey, default: []].append(attributes) } // 遍历每组,调整行内cell的位置实现居中 for (_, rowAttributes) in rowGroups { guard !rowAttributes.isEmpty else { continue } // 计算当前行所有cell的总宽度(包含cell间的间距) let totalCellWidth = rowAttributes.reduce(0.0) { $0 + $1.frame.width } let totalSpacing = minimumInteritemSpacing * CGFloat(rowAttributes.count - 1) let totalRowContentWidth = totalCellWidth + totalSpacing // 计算行的左右偏移量,让整行居中 let collectionViewWidth = collectionView?.bounds.width ?? 0 let sectionInsets = self.sectionInsetForSection(rowAttributes.first!.indexPath.section) let availableWidth = collectionViewWidth - sectionInsets.left - sectionInsets.right let horizontalInset = (availableWidth - totalRowContentWidth) / 2 var currentX = sectionInsets.left + max(horizontalInset, 0) // 避免偏移量为负导致内容溢出 // 逐个调整cell的x坐标 for attributes in rowAttributes { attributes.frame.origin.x = currentX currentX += attributes.frame.width + minimumInteritemSpacing } } return attributesArray } // 适配不同分区的自定义内边距 private func sectionInsetForSection(_ section: Int) -> UIEdgeInsets { if let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout { return delegate.collectionView?(collectionView!, layout: self, insetForSectionAt: section) ?? self.sectionInset } return self.sectionInset } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { // 屏幕旋转等bounds变化时,重新计算布局 return true } }
第二步:在ViewController中使用这个布局
初始化CollectionView时,直接用咱们自定义的布局即可,别忘了设置间距、内边距等参数:
override func viewDidLoad() { super.viewDidLoad() let centeredLayout = CenteredCollectionViewFlowLayout() centeredLayout.minimumInteritemSpacing = 10 // cell之间的水平间距 centeredLayout.minimumLineSpacing = 15 // 行之间的垂直间距 centeredLayout.sectionInset = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16) // 默认分区内边距 let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: centeredLayout) collectionView.delegate = self collectionView.dataSource = self collectionView.register(YourCustomCell.self, forCellWithReuseIdentifier: "cellID") collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(collectionView) }
额外注意点
- 如果你的分区有不同的
sectionInset,上面的代码已经通过sectionInsetForSection方法适配了,只要你在UICollectionViewDelegateFlowLayout的insetForSectionAt方法里返回对应的值就行。 - 不管你的cell是固定大小还是动态大小(通过
sizeForItemAt代理方法设置),这个布局都能正确计算并居中,因为它是基于每个cell的实际frame宽度来计算的。
内容的提问来源于stack exchange,提问作者Hudi Ilfeld
相关产品推荐
相关产品推荐

