具有动态高度和自动布局的 UICollectionViewCell

时间:2021-02-06 23:13:17

标签: ios swift uicollectionview autolayout uicollectionviewcell

我想创建一个自定义 UICollectionView,能够根据其配置以不同的方式做出反应。

我创建了 UICollectionViewFlowLayout 的自定义子类,并将其 estimatedItemSize 设置为 UICollectionViewFlowLayout.automaticSize。该组件还能够根据其配置计算不同的布局宽度。

ColumnFlowLayout.swift

import UIKit

class ColumnFlowLayout: UICollectionViewFlowLayout {
    let columns: Int

    init(columns: Int, insets: UIEdgeInsets) {
        self.columns = columns
        
        super.init()

        self.sectionInsetReference = .fromContentInset
        self.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        self.minimumInteritemSpacing = insets.top
        self.minimumLineSpacing = insets.bottom
        self.sectionInset = insets
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
        layoutAttributesObjects?.forEach({ layoutAttributes in
            if layoutAttributes.representedElementCategory == .cell {
                if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
                    layoutAttributes.frame = newFrame
                }
            }
        })
        return layoutAttributesObjects
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            return nil
        }

        layoutAttributes.frame.size.width = self.estimatedWidth()

        return layoutAttributes
    }
    
    private func estimatedWidth() -> CGFloat {
        guard let collectionView = collectionView else { return 0.0 }
        let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(columns - 1)
        return ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(columns)).rounded(.down)
    }
}

然后我将其用于 UICollectionView 配置。

CollectionViewController.swift

private func prepareCollectionView() {
    let columnLayout = ColumnFlowLayout(columns: Layout.COLUMNS, insets: Layout.INSETS)
    
    self.collectionView!.collectionViewLayout = columnLayout
    self.collectionView!.contentInsetAdjustmentBehavior = .always
            
    ...
    
}

为了使其工作,每个 UICollectionViewCell 都有 preferredLayoutAttributesFitting

BaseCollectionViewCell.swift

import UIKit

class BaseCollectionCell: UICollectionViewCell {
    ...

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
        layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
        return layoutAttributes
    }

    ...
}

输出接近想要的结果,除了顶部间距:

enter image description here

我希望像这样将每个单元格对齐

enter image description here

您有什么建议吗?

2 个答案:

答案 0 :(得分:0)

是的,您可以使用 UICollectionViewFlowLayout 的自定义子类。

这是我的建议:

每一项都是const max frame,你需要自己计算每一列的高度

动态高度和自动布局是项目的子视图

做布局计算

private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
private var contentHeight = CGFloat.zero
override public func prepare(){
     guard let collectionView = collectionView else {
         return
     }
    
     prepareCache()
     contentHeight = 0
     
      var cellX: CGFloat = 0
      let count = collectionView.numberOfItems(inSection: 0)
      for item in 0 ..< count {
          let cellIndexPath = IndexPath(item: item, section: 0)
          let attributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
           // itemSize.width is estimatedWidth()

          // itemSize.height, is max height, 
          // you need calculate the height of each column yourself

          // y: contentHeight, this is what you concerned

          attributes.frame = CGRect( x: cellX, y: contentHeight, width: itemSize.width, height: itemSize.height )
          
          if cellX + itemSize.width * 2 < collectionViewWidth + 1{
              cellX = itemSize.width
          }
          else{
              cellX = 0
              contentHeight += itemSize.height
          }
          cache[cellIndexPath] = attributes
      }
      if count % 2 == 1{
          contentHeight += itemSize.height
      }

      contentHeight += 30 // bottom margin
  }


private func prepareCache() {
    cache.removeAll(keepingCapacity: true)
    cache = [IndexPath: UICollectionViewLayoutAttributes]()

  }

常见模式

override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return cache[indexPath]
  }
  

  override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    guard let _ = collectionView else { return nil }
    
    visibleLayoutAttributes.removeAll(keepingCapacity: true)
    for (_, attributes) in cache {
        attributes.transform = .identity
        if attributes.frame.intersects(rect) {
            visibleLayoutAttributes.append(attributes)
        }
    }
    return visibleLayoutAttributes
  }

答案 1 :(得分:0)

我在此 thread 之后找到了一个解决方案。

我还不知道是否会有极端情况,但我认为这可能是正确的方向。我将 layoutAttributesForElements 更改如下:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)?
        .map { $0.copy() } as? [UICollectionViewLayoutAttributes]
    
    attributes?
        .reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) {
            guard $1.representedElementCategory == .cell else { return $0 }
            return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) {
                ($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
            }
        }
        .values.forEach { minY, line in
            line.forEach {
                $0.frame = $0.frame.offsetBy(
                    dx: 0,
                    dy: minY - $0.frame.origin.y
                )
                $0.frame.size.width = self.estimatedWidth()
            }
        }
    
    return attributes
}

它似乎工作正常:

enter image description here

相关问题