我想创建一个自定义 UICollectionView,能够根据其配置以不同的方式做出反应。
我创建了 UICollectionViewFlowLayout
的自定义子类,并将其 estimatedItemSize
设置为 UICollectionViewFlowLayout.automaticSize
。该组件还能够根据其配置计算不同的布局宽度。
ColumnFlowLayout.swift
import UIKit
class ColumnFlowLayout: UICollectionViewFlowLayout {
let columns: Int
init(columns: Int, insets: UIEdgeInsets) {
self.columns = columns
super.init()
self.sectionInsetReference = .fromContentInset
self.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
self.minimumInteritemSpacing = insets.top
self.minimumLineSpacing = insets.bottom
self.sectionInset = insets
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
layoutAttributesObjects?.forEach({ layoutAttributes in
if layoutAttributes.representedElementCategory == .cell {
if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
})
return layoutAttributesObjects
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
return nil
}
layoutAttributes.frame.size.width = self.estimatedWidth()
return layoutAttributes
}
private func estimatedWidth() -> CGFloat {
guard let collectionView = collectionView else { return 0.0 }
let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(columns - 1)
return ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(columns)).rounded(.down)
}
}
然后我将其用于 UICollectionView 配置。
CollectionViewController.swift
private func prepareCollectionView() {
let columnLayout = ColumnFlowLayout(columns: Layout.COLUMNS, insets: Layout.INSETS)
self.collectionView!.collectionViewLayout = columnLayout
self.collectionView!.contentInsetAdjustmentBehavior = .always
...
}
为了使其工作,每个 UICollectionViewCell
都有 preferredLayoutAttributesFitting
BaseCollectionViewCell.swift
import UIKit
class BaseCollectionCell: UICollectionViewCell {
...
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return layoutAttributes
}
...
}
输出接近想要的结果,除了顶部间距:
我希望像这样将每个单元格对齐:
您有什么建议吗?
答案 0 :(得分:0)
是的,您可以使用 UICollectionViewFlowLayout 的自定义子类。
这是我的建议:
每一项都是const max frame,你需要自己计算每一列的高度
动态高度和自动布局是项目的子视图
做布局计算
private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
private var contentHeight = CGFloat.zero
override public func prepare(){
guard let collectionView = collectionView else {
return
}
prepareCache()
contentHeight = 0
var cellX: CGFloat = 0
let count = collectionView.numberOfItems(inSection: 0)
for item in 0 ..< count {
let cellIndexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
// itemSize.width is estimatedWidth()
// itemSize.height, is max height,
// you need calculate the height of each column yourself
// y: contentHeight, this is what you concerned
attributes.frame = CGRect( x: cellX, y: contentHeight, width: itemSize.width, height: itemSize.height )
if cellX + itemSize.width * 2 < collectionViewWidth + 1{
cellX = itemSize.width
}
else{
cellX = 0
contentHeight += itemSize.height
}
cache[cellIndexPath] = attributes
}
if count % 2 == 1{
contentHeight += itemSize.height
}
contentHeight += 30 // bottom margin
}
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache = [IndexPath: UICollectionViewLayoutAttributes]()
}
常见模式
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath]
}
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let _ = collectionView else { return nil }
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (_, attributes) in cache {
attributes.transform = .identity
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
答案 1 :(得分:0)
我在此 thread 之后找到了一个解决方案。
我还不知道是否会有极端情况,但我认为这可能是正确的方向。我将 layoutAttributesForElements
更改如下:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?
.map { $0.copy() } as? [UICollectionViewLayoutAttributes]
attributes?
.reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) {
guard $1.representedElementCategory == .cell else { return $0 }
return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) {
($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
}
}
.values.forEach { minY, line in
line.forEach {
$0.frame = $0.frame.offsetBy(
dx: 0,
dy: minY - $0.frame.origin.y
)
$0.frame.size.width = self.estimatedWidth()
}
}
return attributes
}
它似乎工作正常: