【问题标题】:UICollectionViewCell with dynamic height and Auto Layout具有动态高度和自动布局的 UICollectionViewCell
【发布时间】:2021-05-10 22:58:44
【问题描述】:

我想创建一个自定义 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
    }

    ...
}

输出接近预期的结果,除了顶部间距:

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

你有什么建议吗?

【问题讨论】:

    标签: ios swift uicollectionview autolayout uicollectionviewcell


    【解决方案1】:

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

    看来它工作正常:

    【讨论】:

      【解决方案2】:

      是的,您可以使用 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
        }
      

      【讨论】:

      • 感谢您的回答。我尝试了这种方法,但我遇到了一个问题。当我手动计算时,我无法根据自动布局获得正确的 UICollectionView 单元格高度。
      猜你喜欢
      • 2014-09-20
      • 2023-04-02
      • 2021-10-12
      • 1970-01-01
      • 1970-01-01
      • 2013-07-04
      • 2015-09-17
      • 2017-04-07
      • 2013-05-25
      相关资源
      最近更新 更多