本文最后更新于 2021年4月4日 晚上
这次遇到一个 Tag 布局的需求, 寻找多方, 看到的很多方法都是自己硬头计算, 不太实用且成本太大. 我在之前的这篇文章讲到过如何在AutoLayout
+ UICollectionViewFlowLayout
的情况下实现集合视图高度撑开, 这次再来看看在 AutoLayout
+ 自定义布局的情况下实现高度撑开, 并实现 Tag 布局.(想了解集合视图自定义布局的内容, 请看这篇文章)
基本环境
问题描述(需求): 在滚动视图(UIScrollView
)中添加若干个集合视图, 且集合视图的高度就是自身内容高度(即单个集合视图不能滚动), 且这些集合视图使用的是自定义布局, 需要将集合视图高度撑开到自身内容的高度, 且布局需要实现 Tag 效果(即左对齐方式).
故上下文环境为:
实现
针对上述问题, 网上的解决方式五花八门多种多样, 但总是没有一个统一的, 简便的且高效的解决方案, 无意中发现这个库, 并且顺藤摸瓜看到 StackOverflow 上的这篇问答, 才初见端倪.
总地来说, 能用 UICollectionViewFlowLayout
来做就尽量用它, 或去继承实现它, 不然会多很多工作量.
自定义 FlowLayout, 主要是重写计算 Attributes 的方法, 思路是: 针对每一个 Item, 修改它和上一个 Item 的相对位置, 即可实现自己需要的左对齐布局.
核心代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let collectionView = collectionView else { return nil } let currentAttr = super.layoutAttributesForItem(at: indexPath)
let sectionInset = getSectionInsetAtIndexPath(indexPath)
let isFirstItemInsection = indexPath.item == 0 guard !isFirstItemInsection else { currentAttr?.leftAlignFrameWithSectionInset(inset: sectionInset) return currentAttr }
let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section) guard let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame, var currentFrame = currentAttr?.frame else { return nil }
let availableTotalWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right let strecthedFrame = CGRect(x: sectionInset.left, y: currentFrame.origin.y, width: availableTotalWidth, height: currentFrame.height)
guard strecthedFrame.intersects(previousFrame) else { currentAttr?.leftAlignFrameWithSectionInset(inset: sectionInset) return currentAttr }
let previousItemRightMostX = previousFrame.maxX currentFrame.origin.x = previousItemRightMostX + getMinimumInteritemSpacingForSectionAtIndex(indexPath.section) currentAttr?.frame = currentFrame return currentAttr }
|
在上面代码中用到了 UICollectionViewLayoutAttributes
的扩展:
1 2 3 4 5
| extension UICollectionViewLayoutAttributes { func leftAlignFrameWithSectionInset(inset: UIEdgeInsets) { self.frame.origin.x = inset.left } }
|
另外获取当前 Item 间隔设置和 Section 间隔设置的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private func getSectionInsetAtIndexPath(_ indexPath: IndexPath) -> UIEdgeInsets { guard let collectionView = collectionView, let flowLayoutDelegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout, let inset = flowLayoutDelegate.collectionView?( collectionView, layout: self, insetForSectionAt: indexPath.section ) else { return sectionInset } return inset }
private func getMinimumInteritemSpacingForSectionAtIndex(_ sectionIndex: Int) -> CGFloat { guard let collectionView = collectionView, let flowLayoutDelegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout, let spacing = flowLayoutDelegate.collectionView?( collectionView, layout: self, minimumInteritemSpacingForSectionAt: sectionIndex ) else { return minimumInteritemSpacing } return spacing }
|
上面重写了 layoutAttributesForItem
方法, 就需要对应修改 layoutAttributesForElements(in rect:)
:
1 2 3 4 5 6 7 8 9 10
| override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard var attributesArray = super.layoutAttributesForElements(in: rect) else { return nil } for (index, attr) in attributesArray.enumerated() where attr.representedElementKind == nil { if let attributes = self.layoutAttributesForItem(at: attr.indexPath) { attributesArray[index] = attributes } } return attributesArray }
|
通过这个布局, 就可以实现所需效果, 简单实用:
下面是整体演示(为便于演示整个撑开过程, 添加了动画效果):