计算 CollectionView 马赛克布局的单元格大小

Posted

技术标签:

【中文标题】计算 CollectionView 马赛克布局的单元格大小【英文标题】:Calculating Size of Cell for CollectionView Mosaic Layout 【发布时间】:2020-04-18 15:13:26 【问题描述】:

我正在尝试制作类似于 Google 的 Keep 应用的马赛克集合视图布局。我对UICollectionViewLayout 进行了子类化,类似于在网上找到的许多教程。为了正确布局集合视图单元格,调用类必须实现一个委托HeightForCellAtIndexPath 方法来获取单元格的高度。就我而言,我还获取单元格的宽度来创建 1、2 或 3 列布局。

在所有教程中,单元格内容的高度都是已知的,不需要计算。就我而言,内容的大小是未知的,需要计算。我尝试了许多不同的计算方法,但没有一个能完美运行。我最近的尝试是创建一个CardContent 类并将其添加到cellForItemAt 中的单元格contentView 中,并在HeightForCellAtIndexPath 中实例化一个CardContent 实例以计算传递给布局类的内容的大小。

我确信我的方法存在很多问题,但据我所知,问题似乎在于HeightForCellAtIndexPath 中的多行标签布局不正确,因为标签没有换行到多行行并保持为单行,因此给我的contentView 的高度不正确。

CardContentCell.swift

import UIKit

class CardContentCell: UICollectionViewCell 
    var todoList: TodoList! 
        didSet 
            self.backgroundColor = UIColor(todoList.color)
        
    

    override init(frame: CGRect) 
        super.init(frame: frame)
        self.layer.cornerRadius = 5.0
    

    required init?(coder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

CardContent.swift

编辑:添加了createLineItem 方法。请参阅下面的答案。

class CardContent: UIStackView 

    var todoList: TodoList!

    var verticalItemSpacing: CGFloat = 10.0

    var cellWidth: CGFloat!


    init(todoList: TodoList, cellWidth: CGFloat = 0.0) 
        self.todoList = todoList
        self.cellWidth = cellWidth

        super.init(frame: CGRect(x: 0, y: 0, width: cellWidth, height: 0))

        self.axis = .vertical
        self.alignment = .fill
        self.distribution = .fill
        self.contentMode = .scaleToFill

        self.spacing = 10.0

        layoutContent()
    

    required init(coder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    func createTitleLabel(title: String) -> UILabel 
        let label = UILabel()
        label.text = title
        label.font = label.font.withSize(20.0)
        label.numberOfLines = 2
        label.lineBreakMode = .byTruncatingTail
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    

    func createItemLabel(text: String) -> UILabel 
        let label = UILabel()
        label.text = text
        label.font = label.font.withSize(17.0)
        label.numberOfLines = 3
        label.lineBreakMode = .byTruncatingTail
        label.translatesAutoresizingMaskIntoConstraints = false
        label.sizeToFit()
        return label
    

    func createLineItem(text: String) -> UIStackView 
        let hstack = UIStackView()
        hstack.axis = .horizontal
        hstack.alignment = .fill
        hstack.distribution = .fillProportionally

        let imgView = createImgView(withFont: lineItemFont)
        let textLabel = createItemLabel(text: text)

        hstack.addArrangedSubview(imgView)
        hstack.addArrangedSubview(textLabel)

        return hstack
    

    func layoutContent() 

        self.addArrangedSubview(createTitleLabel(title: todoList.title))

        for todo in todoList.todos.prefix(6) 
            let lineItem = createLineItem(text: todo.text)
            self.addArrangedSubview(lineItem)

        
    


MyCollectionView.swift

extension MyCollectionView: UICollectionViewDataSource 

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! CardContentCell
        cell.todoList = todoLists[indexPath.row]
        let content = CardContent(todoList: cell.todoList)
        cell.contentView.addSubview(content)
        content.pinTopAndSides(to: cell.contentView) // See extension below

        return cell
    



extension MyCollectionView: CardLayoutDelegate 
    func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat 

        let todoList = todoLists[indexPath.row]
        let stackView = CardContent(todoList: todoList, cellWidth: cellWidth)

        stackView.translatesAutoresizingMaskIntoConstraints = false

        stackView.setNeedsLayout()
        stackView.layoutIfNeeded()

        let size = stackView.frame.size

        return size.height
    


extension UIView 

    func pinTopAndSides(to other: UIView) 
        translatesAutoresizingMaskIntoConstraints = false
        leadingAnchor.constraint(equalTo: other.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: other.topAnchor).isActive = true
    

结果是,如果总是有 6 个订单项,那么计算的高度总是 230(在 2 列布局中)。在下面的屏幕截图中,单元格被着色,而其余内容溢出。

【问题讨论】:

medium.com/@cp-satish-v/… 【参考方案1】:

除非有更好的解决方案,否则我的答案是不使用嵌套的水平UIStackview。这充满了未知数并且难以诊断自动布局问题。相反,我使用了UIView 并添加了我自己的约束。

这是创建所述视图的方法。有趣的是,没有人仔细研究我的问题,以至于在我匆忙复制和过去的过程中,我在原帖中省略了这个最关键的方法。我将使用此方法的原始实现更新问题以供参考。

func createLineItem(text: String) -> UIView 
    let view = UIView()

    let imgView = createImgView(withFont: lineItemFont)
    imgView.translatesAutoresizingMaskIntoConstraints = false

    let textLabel = createItemLabel(text: text)
    textLabel.translatesAutoresizingMaskIntoConstraints = false

    imgView.tintColor = self.textColor

    view.addSubview(imgView)
    view.addSubview(textLabel)

    imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    imgView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true

    textLabel.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 5.0).isActive = true
    textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    textLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    textLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

    return view

对于HeightForCellAtIndexPath 委托函数,将widthAnchor 设置为单元格宽度提供了正确的单元格高度:

func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat 
    let stackView = CardContent(todoList: todoList)
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.widthAnchor.constraint(equalToConstant: cellWidth).isActive = true
    stackView.setNeedsLayout()
    stackView.layoutIfNeeded()
    let size = stackView.frame.size
    return size.height

【讨论】:

以上是关于计算 CollectionView 马赛克布局的单元格大小的主要内容,如果未能解决你的问题,请参考以下文章

CollectionView缩放水平卡片布局

CollectionView didDeselectItemAt 没有被调用 CollectionView 中的单选

iOS:在Objective C中使用集合视图的马赛克布局[重复]

如何使用自动布局动态调整具有固定宽度的collectionview单元格高度?

CollectionView常用的布局方式总结

CollectionView 里面的 CollectionView | CollectionViewCell 自动布局错误