具有动态高度的 UICollectionViewLayout - 但不使用流布局

Fat*_*tie 6 uicollectionview ios12 swift5

假设您有一个带有普通自定义 UICollectionViewLayout 的 UICollectionView。

所以这不是<<<流布局 - 这是一个正常的自定义布局。

自定义布局很简单,在prepare调用中您只需遍历数据并布置每个矩形即可。所以说这是一个垂直滚动集合......

override func prepare() {
    cache = []
    var y: CGFloat = 0
    let k = collectionView?.numberOfItems(inSection: 0) ?? 0
    // or indeed, just get that direct from your data
    
    for i in 0 ..< k {
        
        // say you have three cell types ...
        let h = ... depending on the cell type, say 100, 200 or 300
        
        let f = CGRect(
            origin: CGPoint(x: 0, y: y ),
            size: CGSize(width: screen width, height: h)
        )
        
        y += thatHeight
        y += your gap between cells
        
        cache.append( .. that one)
    }
}
Run Code Online (Sandbox Code Playgroud)

在示例中,三种单元类型的单元高度都是固定的 - 都没有问题。

如果您使用流布局,则处理动态单元格高度已被充分探索,并且确实相对简单。(例子,也见www上很多解释。)

但是,如果您想要具有(流)完全正常的日常 UICollectionViewLayout 的动态单元格高度怎么办?

估计的ItemSize 在哪里?

据我所知,UICollectionViewLayout 中没有estimatedItemSize 概念?

那你到底要做什么?

您可以天真地只是 - 在上面的代码中 - 简单地以一种或另一种方式计算每个单元格的最终高度(例如计算任何文本块的高度等)。但这似乎效率很低:在计算出全部 100 个单元格大小之前,无法绘制集合视图中的任何内容。你根本不会使用任何 iOS 的动态高度功能,而且没有什么是及时的。

我想,您可以从头开始编写整个即时系统。(所以,像 .. 使表大小实际上只有 1,手动计算该高度,将其发送到集合视图;计算项目 2 的高度,将其发送,依此类推。)但这非常蹩脚。

有没有办法使用自定义 UICollectionViewLayout 实现动态高度单元格 - 而不是流布局?

(同样,当然显然你可以手动完成,所以在上面的代码中一次性计算所有 1000 个高度,你就完成了,但这将是非常蹩脚的。)

就像我上面说的第一个难题是,(正常,非流)UICollectionViewLayout 中的“估计大小”概念到底在哪里?

小智 5

只是警告:自定义布局绝非微不足道,它们可能值得自己写一篇研究论文;)

可以在自己的布局中实现尺寸估计和动态调整尺寸。事实上,估计的尺寸并没有什么特别的。相反,动态大小是。然而,由于自定义布局使您可以完全控制所有内容,因此这涉及许多步骤。您需要在布局子类中实现三种方法,并在单元格中实现一种方法。

  1. 首先,您需要preferredLayoutAttributesFitting(_:)在单元格(或者更一般地说,可重用视图子类)中实现。在这里您可以使用任何您想要的计算。您可能会对单元格使用自动布局:如果是这样,您将需要将所有单元格的子视图添加到其contentView,将它们限制到边缘,然后systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)在这个“首选属性”方法中调用。例如,如果您希望单元格垂直调整大小,同时受到水平限制,则可以编写:

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    
                // Ensures that cell expands horizontally while adjusting itself vertically.
                let preferredSize = systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    
                layoutAttributes.size = preferredSize
                return layoutAttributes
            }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 在向单元格询问其首选属性后,shouldInvalidateLayout(forPreferredLayoutAttributes:withOriginalAttributes:)将调用布局对象上的 。重要的是,您不能只是简单地键入return true,因为系统将无限期地重新询问单元格。这实际上非常聪明,因为许多单元格可能会对彼此的更改做出反应,因此布局最终决定是否满足单元格的愿望。通常,为了调整大小,您会编写如下内容:

    override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
    
                if preferredAttributes.size.height.rounded() != originalAttributes.size.height.rounded() {
                    return true
                }
                return false
            }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 紧接着,invalidationContext(forPreferredLayoutAttributes:withOriginalAttributes:)就会被叫到。您通常需要自定义上下文类来存储特定于您的布局的信息。但一个重要但相当不直观的警告是,您不应该调用context.invalidateItems(at:)因为这将导致布局使所提供的索引路径中实际可见的那些项目无效。只需跳过此方法,因此布局将重新查询可见矩形。

    然而!您需要彻底考虑是否需要设置contentOffsetAdjustmentcontentSizeAdjustment:如果某些内容调整大小,您的集合视图作为一个整体可能会缩小或扩大。如果您不考虑这些,则在滚动时将会发生跳跃重新加载。

  4. 最后,invalidateLayout(with:)将被调用。此步骤旨在让您实际调整部分/行的高度,移动受调整单元格大小影响的内容等。如果您覆盖,则需要调用super.

PS:这确实是一个很难的话题,我只是触及了表面。你可以在这里看看它有多复杂(但这个仓库也是一个非常丰富的学习工具)。