如何实现水平UICollectionView动态宽度单元格等间距布局
问题根源
你直接照搬了垂直滚动UICollectionView的等间距布局逻辑,和水平滚动场景的轴向逻辑完全不匹配:
- 垂直滚动等间距布局的核心是调整x坐标,超出视图宽度时自动增加y值换行;但水平滚动场景下内容沿x轴延伸,你原代码的换行判断逻辑完全失效,还保留了系统FlowLayout默认给每个cell生成的换行y偏移,才会出现每放一个cell就占一行的问题
- 你只实现了
layoutAttributesForElements(in:),没有重写单个item的布局属性获取方法、没有处理布局刷新、没有计算正确的内容滚动范围,才会出现部分cell不显示的问题
正确实现代码
如果你的需求是水平滚动、动态宽度cell、cell间固定间距、所有cell在同一行垂直居中,可以直接用下面的实现:
class TagsLayout: UICollectionViewFlowLayout { let cellSpacing: CGFloat = 20 override init(){ super.init() scrollDirection = .horizontal // 开启动态宽度计算 estimatedItemSize = UICollectionViewFlowLayout.automaticSize minimumLineSpacing = 0 minimumInteritemSpacing = 0 } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! scrollDirection = .horizontal estimatedItemSize = UICollectionViewFlowLayout.automaticSize minimumLineSpacing = 0 minimumInteritemSpacing = 0 } override func prepare() { super.prepare() // 计算垂直边距让所有cell居中显示 guard let collectionView = collectionView else { return } let verticalInset = (collectionView.bounds.height - estimatedItemSize.height) / 2 sectionInset = UIEdgeInsets(top: verticalInset, left: 16, bottom: verticalInset, right: 16) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let attributes = super.layoutAttributesForElements(in: rect), let attributesToReturn = attributes.map({ $0.copy() }) as? [UICollectionViewLayoutAttributes], let collectionView = collectionView else { return nil } var currentX = sectionInset.left let centerY = collectionView.bounds.midY // 按item序号排序,从左到右依次排列 for attr in attributesToReturn.sorted(by: { $0.indexPath.item < $1.indexPath.item }) { // 重置y坐标,所有cell垂直居中,彻底解决意外换行问题 attr.center.y = centerY attr.frame.origin.x = currentX // 累加x偏移 currentX = attr.frame.maxX + cellSpacing } return attributesToReturn } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attr = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes, let targetAttr = layoutAttributesForElements(in: attr.frame)?.first(where: { $0.indexPath == indexPath }) else { return super.layoutAttributesForItem(at: indexPath) } return targetAttr } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { // 边界变化时自动刷新布局,避免旋转、尺寸变更时布局错乱 return true } override var collectionViewContentSize: CGSize { guard let collectionView = collectionView, let itemCount = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0), itemCount > 0 else { return .zero } // 计算总内容宽度,保证最后一个cell可以完整滚动到可见区域 var totalWidth = sectionInset.left + sectionInset.right for i in 0..<itemCount { let indexPath = IndexPath(item: i, section: 0) if let attr = layoutAttributesForItem(at: indexPath) { totalWidth += attr.frame.width if i != itemCount - 1 { totalWidth += cellSpacing } } } return CGSize(width: totalWidth, height: collectionView.bounds.height) } }
关键修改点
- 删除了原代码中适配垂直滚动的换行判断逻辑,水平单行场景下直接按item序号从左到右排列即可
- 统一设置所有cell的y轴中心点为collectionView垂直中点,消除系统默认生成的换行y偏移
- 重写
layoutAttributesForItem(at:),保证滚动过程中系统单独请求单个cell布局时返回正确属性,解决部分cell不显示的问题 - 重写
shouldInvalidateLayout(forBoundsChange:),保证视图尺寸变化、滚动时布局自动刷新 - 重写
collectionViewContentSize计算准确的内容宽度,避免最后一个cell被截断、无法滚动到可见区域的问题
异常效果参考

内容的提问来源于stack exchange,提问作者ED.
相关产品推荐
相关产品推荐

