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

Mic*_*all 14 uikit ios uicollectionview uicollectionviewlayout

我正在构建一个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
    }

}
Run Code Online (Sandbox Code Playgroud)

这是单元格首选的布局属性计算(注意我们让布局决定并修复宽度,我们要求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
    }
}
Run Code Online (Sandbox Code Playgroud)

在布局逻辑中是否有明显的原因导致这种情况?

J.H*_*ter 5

我通过set重复了这个问题

estimatedItemHeight = 500
Run Code Online (Sandbox Code Playgroud)

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

return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
Run Code Online (Sandbox Code Playgroud)

在功能框架中与.相同

return $0 + self.spacing + self.estimatedItemHeight
Run Code Online (Sandbox Code Playgroud)

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

self.heightCache[index] = preferredAttributes.size.height
Run Code Online (Sandbox Code Playgroud)

在功能上

invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext
Run Code Online (Sandbox Code Playgroud)

as preferredAttributes.size.height始终为零

finalHeight

在课堂上也是零

细胞


Tom*_*ing 0

我同样试图建立一个自定义UICollectionViewLayout我同样试图为UITableView子类,但我遇到了一个略有不同的问题。但我发现shouldInvalidateLayoutForPreferredLayoutAttributes,如果您根据首选高度是否与原始高度匹配(而不是首选高度是否与缓存中的高度匹配)返回,它将正确应用布局属性,并且所有单元格都将具有正确的高度。

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

请参阅 GitHub上的示例项目。

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