具有自动调整大小的单元格的自定义UICollectionViewLayout以较大的估计项目高度中断

时间:2018-04-13 20:41:32

标签: ios uicollectionview uikit uicollectionviewlayout

我正在构建支持自动调整大小单元格的自定义UICollectionViewLayout,当估计的项目高度大于最终高度时,我遇到了问题。当首选布局属性触发部分失效时,下面的一些单元格变为可见,并非所有单元格都应用了正确的框架。

在下图中,左侧屏幕截图显示了具有较大估计高度的初始渲染,右侧图像显示了估计高度小于最终高度的位置。

iOS 10和11上会出现此问题。

使用较小的估计高度,布局期间内容大小会增加,并且首选布局属性不会导致更多项目移动到可见矩形中。集合视图可以完美地处理这种情况。

失效和帧计算的逻辑似乎有效,所以我不确定为什么集合视图不处理部分失效导致新项目进入视图的情况。

当检查更深时,似乎将被移动到视图中的最终视图无效并被要求计算它们的大小,但它们的最终属性未被应用。

enter image description here

这是一个非常精简的自定义布局版本的布局代码,用于演示这个故障:

/// 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
    }

}

这是单元格首选的布局属性计算(注意我们让布局决定并修复宽度,我们要求autolayout计算给定宽度的单元格高度)。

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
    }
}

在布局逻辑中是否有明显的原因?

2 个答案:

答案 0 :(得分:5)

我已经通过set

重复了这个问题
estimatedItemHeight = 500

在演示代码中。我有一个关于你为每个单元格计算框架的逻辑的问题:self.heightCache中的所有高度都是零,所以语句

return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
功能框中的

相同
return $0 + self.spacing + self.estimatedItemHeight

我想也许你应该查看这段代码

self.heightCache[index] = preferredAttributes.size.height

在功能

invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext

as preferredAttributes.size.height始终为零

  

finalHeight

在班级

中也为零
  

细胞

答案 1 :(得分:0)

我同样正在尝试为UICollectionViewLayout样式的布局构建自定义UITableView子类,而我遇到的是slightly different problem。但是我发现在shouldInvalidateLayoutForPreferredLayoutAttributes中,如果您根据首选高度是否与原始高度匹配(而不是首选高度是否与缓存中的高度匹配)返回,它将正确应用布局属性和您所有的单元格都将具有正确的高度。

但是随后您会丢失单元格,因为在发生自调整大小并修改了单元格高度(因此更改了y位置)后,您并不总是需要重新查询layoutAttributesForElementsInRect

在GitHub上查看示例项目here

编辑:我的问题已得到解答,并且GitHub上的示例正在运行。