我的目标是在 UICollectionView
中拥有动态大小的单元格。此外,当UICollectionView
的可滚动contentWidth
小于集合的bounds
时,我希望项目在集合内居中。
到目前为止的方法:
我正在尝试以尽可能最干净的方式做到这一点。我有自动调整大小的 UICollectionViewCells,其中自动布局可以根据内容的约束确定单元格的大小。为此,我的单元格重写了 preferredLayoutAttributesFitting
函数,返回其内容的适当宽度。
我的自定义 UICollectionViewFlowLayout
子类使用 UICollectionViewCell
提供的宽度信息来正确调整单元格的大小。
在我的自定义 UICollectionViewFlowLayout
上重写的唯一方法是:
layoutAttributesForItem(at...)
layoutAttributesForElementsIn(矩形...)
shouldInvalidateLayout(forBoundsChange...)
UICollectionViewCells
的动态大小调整工作完美。
问题:
使用这种方法,我事先不知道每个单元格的大小(每个单元格可以不同)。我想实现 UICollectionView
的 delegate
方法来提供一个插图,使我的内容在 Collection View 中居中。为此,我想我应该实现并覆盖
collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
问题在于 inset 方法是在布局过程的早期调用的,我不知道布局期间将确定的 contentSize
。
以下是单个完整布局过程的各种布局方法的堆栈,按它们运行的顺序排列:
prepare started
prepare finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
prepare started
insetForSectionAt 0 called <--- The datasource has knowledge of the presence of cells but the cells themselves are not yet dequeued
prepare finished
layoutAttributesForElementsInRect started
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished
prepare started
prepare finished
layoutAttributesForElementsInRect started
<--- At this point Cells DO return valid sizes through Autolayout
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished
对于单个布局过程,会多次调用各种布局方法,但只有在最后才能将来自不同单元格的自动布局的大小信息传递到布局。
我注意到,在单元格正确出列并允许运行其自动布局约束之前,sectionInset
委托(delegate)函数被提前调用。因此,在调用时不可能告知 inset 正确的值。
使用这种方法,是否可以在布局传递后强制调用重新计算 sectionInsetAt
或者我应该完全更改技术?
我尝试过的事情:
我尝试从 insetForSectionAt
委托(delegate)函数中查询 collectionViewLayout
的 collectionViewContentSize
。不幸的是,这不能在布局过程中使用,因为布局当前正在进行中。它会导致崩溃并出现错误:
[CollectionView] An attempt to update layout information was detected while already in the process of computing the layout (i.e. reentrant call). This will result in unexpected behaviour or a crash. This may happen if a layout pass is triggered while calling out to a delegate. UICollectionViewFlowLayout instance is (<App.ViewportToolbarFlowLayout: 0x7f8a5261add0>)
我尝试使用collectionView
的contentSize
方法来获取insetForSectionAt
委托(delegate)方法执行期间的大小,但它确实在布局过程中运行时不提供正确的数据。
最佳答案
在这里发布答案。我发现this answer作者:Alex Koshy,这对我帮助很大。
基本上,该工作必须在 layoutAttributesForElementsIn(rect...)
中执行,因为它是唯一可用的完整布局图片的地方。我已经调整了 Alex 的代码来处理水平和垂直的 UICollectionViews(这可能不是每个人都想要的,但正是我需要的)。我使用布局的 scrollDirection
属性来确定元素是水平居中(当 scrollDirection
为 .horizontal
时)还是垂直居中(当 >scrollDirection
是 .vertical
)。
这是代码:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute -> UICollectionViewLayoutAttributes? in
return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
}
guard let attributes = NSArray(array: computedAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil }
let interItemSpacing = minimumInteritemSpacing
switch scrollDirection {
case .horizontal:
let leftPadding: CGFloat = self.sectionInset.left
var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item
var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item
var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row
var currentRow: Int = 0 // Tracks the current row
attributes.forEach { layoutAttribute in
// Each layoutAttribute represents its own item
if layoutAttribute.frame.origin.y >= maxY {
// This layoutAttribute represents the left-most item in the row
leftMargin = leftPadding
currentRow += rowSizes.isEmpty ? 0 : 1
// Register its origin.x in rowSizes for use later
rowSizes.append([leftMargin, 0])
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + interItemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
// Add right-most x value for last item in the row
rowSizes[currentRow][1] = leftMargin - interItemSpacing
}
// At this point, all cells are left aligned
// Reset tracking values and add extra left padding to center align entire row
leftMargin = leftPadding
maxY = -1.0
currentRow = 0
attributes.forEach { (layoutAttribute) in
if layoutAttribute.frame.origin.y >= maxY {
// This layoutAttribute represents the left-most item in the row
leftMargin = leftPadding
// Need to bump it up by an appended margin
let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x
let appendedMargin = (collectionView.frame.width - leftPadding - rowWidth - leftPadding) / 2
leftMargin += appendedMargin
currentRow += 1
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + interItemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
}
case .vertical:
let topPadding: CGFloat = self.sectionInset.top
var topMargin: CGFloat = topPadding
var maxX: CGFloat = -1.0 // Modified to determine origine.x for each item
var colSizes: [[CGFloat]] = [] // Tracks the starting and ending y-values for the first and last item in the column
var currentCol: Int = 0
attributes.forEach { (layoutAttribute) in
if layoutAttribute.frame.origin.x >= maxX {
topMargin = topPadding
currentCol += colSizes.isEmpty ? 0 : 1
colSizes.append([topMargin, 0])
}
layoutAttribute.frame.origin.y = topMargin
topMargin += layoutAttribute.frame.height + interItemSpacing
maxX = max(layoutAttribute.frame.maxX, maxX)
colSizes[currentCol][1] = topMargin - interItemSpacing
}
topMargin = topPadding
maxX = -1.0
currentCol = 0
attributes.forEach { (layoutAttribute) in
if layoutAttribute.frame.origin.x >= maxX {
topMargin = topPadding
let colWidth = colSizes[currentCol][1] - colSizes[currentCol][0]
let appendedMargin = (collectionView.frame.height - topPadding - colWidth - topPadding) / 2
topMargin += appendedMargin
currentCol += 1
}
layoutAttribute.frame.origin.y = topMargin
topMargin += layoutAttribute.frame.height + interItemSpacing
maxX = max(layoutAttribute.frame.maxX, maxX)
}
}
return attributes
}
关于ios - 如何使用动态大小的单元格在 UICollectionViewFlowLayout 子类上正确设置部分插入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55078289/