具有自动调整大小的单元格的自定义 UICollectionViewLayout 会因估计的项目高度较大而中断
Posted
技术标签:
【中文标题】具有自动调整大小的单元格的自定义 UICollectionViewLayout 会因估计的项目高度较大而中断【英文标题】:Custom UICollectionViewLayout w/ auto-sizing cells breaks with larger estimated item heights 【发布时间】:2018-04-13 20:41:32 【问题描述】:我正在构建一个支持自动调整单元格大小的自定义 UICollectionViewLayout
,当估计的项目高度大于最终高度时,我遇到了一个问题。当首选布局属性触发部分失效时,下面的某些单元格变得可见,但并非所有单元格都应用了正确的框架。
在下图中,左侧屏幕截图显示了估计高度较大的初始渲染,右侧图像显示了估计高度小于最终高度的位置。
此问题出现在 ios 10 和 11 上。
估计高度越小,布局期间内容大小会增加,首选布局属性不会导致更多项目移动到可见矩形中。集合视图完美地处理了这种情况。
失效和框架计算的逻辑似乎是有效的,所以我不确定为什么集合视图没有处理部分失效导致新项目进入视图的情况。
当更深入地检查时,似乎应该被移入视图的最终视图正在失效并被要求计算它们的大小,但它们的最终属性没有被应用。
这是一个非常精简的自定义布局版本的布局代码,用于展示此故障的演示:
/// Simple demo layout, only 1 section is supported
/// This is not optimised, it is purely a simplified version
/// of a more complex custom layout that demonstrates
/// the glitch.
public class Layout: UICollectionViewLayout
public var estimatedItemHeight: CGFloat = 50
public var spacing: CGFloat = 10
var contentWidth: CGFloat = 0
var numberOfItems = 0
var heightCache = [Int: CGFloat]()
override public func prepare()
super.prepare()
self.contentWidth = self.collectionView?.bounds.width ?? 0
self.numberOfItems = self.collectionView?.numberOfItems(inSection: 0) ?? 0
override public var collectionViewContentSize: CGSize
// Get frame for last item an duse maxY
let lastItemIndex = self.numberOfItems - 1
let contentHeight = self.frame(for: IndexPath(item: lastItemIndex, section: 0)).maxY
return CGSize(width: self.contentWidth, height: contentHeight)
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
// Not optimal but works, get all frames for all items and calculate intersection
let attributes: [UICollectionViewLayoutAttributes] = (0 ..< self.numberOfItems)
.map IndexPath(item: $0, section: 0)
.compactMap indexPath in
let frame = self.frame(for: indexPath)
guard frame.intersects(rect) else
return nil
let attributesForItem = self.layoutAttributesForItem(at: indexPath)
return attributesForItem
return attributes
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = self.frame(for: indexPath)
return attributes
public func frame(for indexPath: IndexPath) -> CGRect
let heightsTillNow: CGFloat = (0 ..< indexPath.item).reduce(0)
return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
let height = self.heightCache[indexPath.item] ?? self.estimatedItemHeight
let frame = CGRect(
x: 0,
y: heightsTillNow,
width: self.contentWidth,
height: height
)
return frame
override public func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool
let index = originalAttributes.indexPath.item
let shouldInvalidateLayout = self.heightCache[index] != preferredAttributes.size.height
return shouldInvalidateLayout
override public func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext
let context = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let index = originalAttributes.indexPath.item
let oldContentSize = self.collectionViewContentSize
self.heightCache[index] = preferredAttributes.size.height
let newContentSize = self.collectionViewContentSize
let contentSizeDelta = newContentSize.height - oldContentSize.height
context.contentSizeAdjustment = CGSize(width: 0, height: contentSizeDelta)
// Everything underneath has to be invalidated
let indexPaths: [IndexPath] = (index ..< self.numberOfItems).map
return IndexPath(item: $0, section: 0)
context.invalidateItems(at: indexPaths)
return context
这是单元格的首选布局属性计算(请注意,我们让布局决定并固定宽度,并且我们要求自动布局计算给定宽度的单元格的高度)。
public class Cell: UICollectionViewCell
// ...
public override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
let finalWidth = layoutAttributes.bounds.width
// With the fixed width given by layout, calculate the height using autolayout
let finalHeight = systemLayoutSizeFitting(
CGSize(width: finalWidth, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
).height
let finalSize = CGSize(width: finalWidth, height: finalHeight)
layoutAttributes.size = finalSize
return layoutAttributes
在布局逻辑中是否存在明显的原因?
【问题讨论】:
我想我有一个非常相似的问题here。你解决过你的问题吗? 另外,我 discovered 在单元格上调用systemLayoutSizeFittingSize
不一定能达到您的预期。但是在contentView
上调用它。
systemLayoutSizeFitting
是做什么的?我读过文档,但不明白。在 WWDC 上也找不到任何东西...
【参考方案1】:
我已经通过 set 复制了这个问题
estimatedItemHeight = 500
在演示代码中。我对你为每个cell:
计算框架的逻辑有疑问,self.heightCache
中的所有高度都为零,所以声明
return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
在函数frame
中与
return $0 + self.spacing + self.estimatedItemHeight
我想也许你应该检查一下这段代码
self.heightCache[index] = preferredAttributes.size.height
在函数中
invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext
因为preferredAttributes.size.height
总是为零
和
最终高度
在类中也是零
单元格
【讨论】:
【参考方案2】:我同样在尝试为UITableView
样式的布局构建一个自定义的UICollectionViewLayout
子类,并且我遇到了slightly different problem。但我发现在shouldInvalidateLayoutForPreferredLayoutAttributes
中,如果您根据首选高度是否与原始高度匹配(而不是首选高度是否与您缓存中的高度匹配)返回,它将正确应用布局属性和您的所有单元格将具有正确的高度。
但是随后您会丢失单元格,因为在发生自动调整大小并修改单元格高度(以及因此 y 位置)之后,您并不总是被重新查询 layoutAttributesForElementsInRect
。
在 GitHub 上查看示例项目 here。
编辑:我的问题得到了解答,GitHub 上的示例现在正在运行。
【讨论】:
以上是关于具有自动调整大小的单元格的自定义 UICollectionViewLayout 会因估计的项目高度较大而中断的主要内容,如果未能解决你的问题,请参考以下文章