【问题标题】:How to properly set section inset on a UICollectionViewFlowLayout subclass with dynamically sized cells如何在具有动态大小的单元格的 UICollectionViewFlowLayout 子类上正确设置节插入
【发布时间】:2019-03-09 14:23:11
【问题描述】:

我的目标是在UICollectionView 中拥有动态大小的单元格。此外,当UICollectionView 的可滚动contentWidth 小于集合的bounds 时,我希望项目在集合中居中。

目前的方法:

我正在尝试以最简洁的方式做到这一点。我有自我调整大小UICollectionViewCells,其中自动布局可以根据内容的约束来确定单元格的大小。为此,我的单元格会覆盖 preferredLayoutAttributesFitting 函数,并为其内容返回适当的宽度。

我的自定义UICollectionViewFlowLayout 子类使用UICollectionViewCell 提供的宽度信息来正确调整单元格的大小。

在我的自定义 UICollectionViewFlowLayout 上覆盖的唯一方法是:

  • layoutAttributesForItem(at...)
  • layoutAttributesForElementsIn(rect...)
  • shouldInvalidateLayout(forBoundsChange...)

UICollectionViewCells 的动态调整大小完美无缺。

问题:

使用这种方法,我不知道每个单元格的大小(每个单元格都可以不同)。我想实现UICollectionViewdelegate 方法来提供一个插入以使我的内容在集合视图中居中。为此,我认为我应该实施并覆盖

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

对于单个布局传递,对各种布局方法进行多次调用,但仅在最后,来自不同单元格的 Auto-layout 的大小信息可用于传递到布局。

我注意到sectionInset 委托函数在单元被正确出列并允许运行其自动布局约束之前被提前调用。因此,在 inset 被调用的时候不可能通知它正确的值。

使用这种方法,是否可以在布局通过后强制调用重新计算sectionInsetAt,还是我应该完全改变技术?

我尝试过的事情:

我尝试从insetForSectionAt 委托函数中查询collectionViewLayoutcollectionViewContentSize。不幸的是,这不能在布局过程中使用,因为布局当前正在进行中。它会导致崩溃并出现错误:

[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>)

我尝试使用collectionViewcontentSize 方法在insetForSectionAt 委托方法执行期间获取大小,但它在布局过程中运行时没有提供正确的数据。

【问题讨论】:

    标签: ios swift uicollectionview


    【解决方案1】:

    在这里发布答案。我找到了 Alex Koshy 的 this answer,这对我很有帮助。

    基本上,工作必须在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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-04-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-13
      • 2011-03-17
      • 2015-03-20
      • 1970-01-01
      相关资源
      最近更新 更多