集合视图中使用 FlowLayout 时的高度自适应 Cell (self sizing cells)
这次遇到一个 Tag 布局的需求, 寻找多方, 看到的很多方法都是自己硬头计算, 不太实用且成本太大. 我在之前的这篇文章讲到过如何在AutoLayout
+ UICollectionViewFlowLayout
的情况下实现集合视图高度撑开, 这次再来看看在 AutoLayout
+ 自定义布局的情况下实现高度撑开, 并实现 Tag 布局.( 想了解集合视图自定义布局的内容, 请看这篇文章)
基本环境
问题描述(需求): 在滚动视图(UIScrollView
)中添加若干个集合视图, 且集合视图的高度就是自身内容高度(即单个集合视图不能滚动), 且这些集合视图使用的是自定义布局, 需要将集合视图高度撑开到自身内容的高度, 且布局需要实现 Tag 效果(即左对齐方式).
故上下文环境为:
-
视图控制器根视图上有滚动视图
-
滚动视图分为几个部分, 内部放置集合视图
-
集合视图高度撑开自己所属的部分
-
集合视图使用
UICollectionViewFlowLayout
的自定义子类对象进行布局
实现
针对上述问题, 网上的解决方式五花八门多种多样, 但总是没有一个统一的, 简便的且高效的解决方案, 无意中发现这个库, 并且顺藤摸瓜看到 StackOverflow 上的这篇问答, 才初见端倪.
总地来说, 能用 UICollectionViewFlowLayout
来做就尽量用它, 或去继承实现它, 不然会多很多工作量.
自定义 FlowLayout, 主 要是重写计算 Attributes 的方法, 思路是: 针对每一个 Item, 修改它和上一个 Item 的相对位置, 即可实现自己需要的左对齐布局.
核心代码如下所示:
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else { return nil }
// super 中也应是通过上一个 Item 的 frame 作为基准计算当前 Item 的布局.
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)
// 若无交集, 则证明是新一行的第一个 Item
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
的扩展:
extension UICollectionViewLayoutAttributes {
func leftAlignFrameWithSectionInset(inset: UIEdgeInsets) {
self.frame.origin.x = inset.left
}
}
另外获取当前 Item 间隔设置和 Section 间隔设置的方法如下:
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:)
:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard var attributesArray = super.layoutAttributesForElements(in: rect) else { return nil }
// 替换所有的 Item 布局对象: 调用重写后的 layoutAttributesForItem 方法生成.
for (index, attr) in attributesArray.enumerated() where attr.representedElementKind == nil {
if let attributes = self.layoutAttributesForItem(at: attr.indexPath) {
attributesArray[index] = attributes
}
}
return attributesArray
}
通过这个布局, 就可以实现所需效果, 简单实用:
下面是整体演示(为便于演示整个撑开过程, 添加了动画效果):