ios UICollection再次查看复杂网格

lokaqttq  于 2023-04-08  发布在  iOS
关注(0)|答案(1)|浏览(111)

我无法得到这样的布局:

我只能在'sizeForItemAt'方法中设置单元格的大小时实现这一点:

我尝试了苹果的解决方案,比如UICollectionViewCompositionalLayout和UICollectionViewLayout的子类化。但是第一个解决方案不能给予设备旋转所需的灵活性,因为你必须设置组中子项的精确计数。UICollectionViewCompositionalLayout的另一个问题是滚动时间计算-它在屏幕显示后不能给出完整的布局。UICollectionViewLayout的子类化(https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts)的性能很糟糕。
但即使有上述方法的所有缺点,我也没有得到我需要的确切布局。我可以想象,我们可以使用一种包含四个单元格的额外类型的单元格,但它也不灵活。
我将感激任何帮助。

wztqucjr

wztqucjr1#

这个布局可以用一个定制的UICollectionViewLayout来完成,而且可能比看起来要简单得多。
首先,将布局想象成每个部分的网格...... 4列x n 行:

因为我们使用的是正方形,所以第一个项目将占用2列和2行。
为了避免宽度/高度混淆和重复,我们将2x2项称为“Primary”项,1x 1项称为“Secondary”项。
因此,当我们计算布局矩形时,我们可以说:

numCols = 4
secondarySize = collectionView.width / numCols

y = 0
row = 0
col = 0

for i in 0..<numItems {

    if i == 0 {

        itemRect = .init(x: 0.0, y: y, width: secondarySize * 2.0, height: secondarySize * 2.0)

        // skip a column
        col = 2

    } else {

        // if we're at the last column
        if col == numCols {
            // increment the row
            row += 1
            // if we're on row 1, next column is 2
            //  else it's 0
            col = row < 2 ? 2 : 0
        }
                
        itemRect = .init(x: col * secondarySize, y: y + row * secondarySize, width: secondarySize, height: secondarySize)
                
        // increment the column
        col += 1
                
    }

}

这很好用,在iPhone 14 Pro Max上给我们这个:

但这并不那么简单,因为当我们旋转手机时,我们不希望这样:

如果我们用的是iPad,我们肯定不想要这个:

所以,我们需要决定我们可以为这个布局做多宽。
当前的电话范围从275到430点宽(纵向方向),所以我们可以说:

  • 如果collectionView宽度小于450,则使用此默认布局
  • 否则
  • 让我们为Primary项使用特定的大小,并“填充”剩余的空间

如果我们决定Primary item为200 x200,则布局代码的初始部分将更改为:

primaryItemSize = 200.0

if contentWidth < 450.0 {
    secondarySize = contentWidth / 4.0
    numCols = 4
} else {
    secondarySize = primaryItemSize / 2.0
    numCols = Int(contentWidth / secondarySize)
}

现在,如果我们的布局看起来像这样(同样,iPhone 14 Pro Max):

旋转电话给我们这个:

iPad看起来是这样的:

我们可能仍然需要一些条件计算...... iPhone SE上的相同代码看起来像这样:

因此,200 x200的主大小对于该设备来说可能太大。
此外,如您所见,设置显式主项目大小不会完全填充宽度。横向方向的iPhone SE的视图宽度为667。如果次要大小(列宽)为100,则6列可以获得600点,最后留下667点的空白空间。
如果这是可以接受的,很好,更少的工作:)否则,我们可以做一个“最佳拟合”计算,要么“增加”一点大小来填充它,要么“缩小”一点大小并扩展到7列。
而且...如果你想要节间距和/或标题,那也需要考虑进去。
这里有一些示例代码来实现这一点:

class SampleViewController: UIViewController {
    
    var collectionView: UICollectionView!
    
    var myData: [[UIImage]] = []
    
    // a view with a "spinner" to show that we are
    //  generating the images to use as the data
    //  (if the data needs to be created in this controller)
    lazy var spinnerView: UIView = {
        let v = UIView()
        let label = UILabel()
        label.text = "Generating Images Data..."
        let spinner = UIActivityIndicatorView(style: .large)
        spinner.startAnimating()
        [label, spinner].forEach { sv in
            sv.translatesAutoresizingMaskIntoConstraints = false
            v.addSubview(sv)
        }
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: v.topAnchor, constant: 20.0),
            label.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 20.0),
            label.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -20.0),
            spinner.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20.0),
            spinner.centerXAnchor.constraint(equalTo: v.centerXAnchor),
            spinner.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -20.0),
        ])
        v.layer.cornerRadius = 8
        v.layer.borderWidth = 1
        v.layer.borderColor = UIColor.black.cgColor
        v.backgroundColor = .white
        return v
    }()
    
    // for development purposes
    var showCellFrame: Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let gl = SampleGridLayout()
        gl.primaryItemSize = 200.0
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: gl)
        
        // the imageView in our SimpleImageCell is inset by 4-points, which results in
        //  8-points between adjacent cells
        // so, if we inset the content 4-points on each side, it will look "balanced"
        //  with a total of 8-points on each side
        collectionView.contentInset = .init(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
        ])
        
        collectionView.register(SimpleImageCell.self, forCellWithReuseIdentifier: SimpleImageCell.identifier)
        
        collectionView.dataSource = self
        collectionView.delegate = self
        
        // for use during development
        let dt = UITapGestureRecognizer(target: self, action: #selector(toggleFraming(_:)))
        dt.numberOfTapsRequired = 2
        view.addGestureRecognizer(dt)
        
        if myData.isEmpty {
            spinnerView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(spinnerView)
            NSLayoutConstraint.activate([
                spinnerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                spinnerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            ])
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // data may already be created by a data manager class
        //  so only create images if needed
        if myData.isEmpty {
            DispatchQueue.global(qos: .userInitiated).async {
                let sectionCounts: [Int] = [
                    8, 2, 3, 4, 5, 10, 13, 16, 24
                ]
                self.myData = SampleData().generateData(sectionCounts)
                DispatchQueue.main.async {
                    self.spinnerView.removeFromSuperview()
                    self.collectionView.reloadData()
                }
            }
        }
        
    }
    
    // for use during development
    @objc func toggleFraming(_ sender: Any?) {
        self.showCellFrame.toggle()
        self.collectionView.reloadData()
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(
            alongsideTransition: { [unowned self] _ in
                self.collectionView.collectionViewLayout.invalidateLayout()
                self.collectionView.reloadData()
            },
            completion: { [unowned self] _ in
                // if we want to do something after the size transition
            }
        )
    }
    
}

// "standard" collection view DataSource funcs
extension SampleViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return myData.count
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData[section].count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: SimpleImageCell.identifier, for: indexPath) as! SimpleImageCell
        
        c.theImageView.image = myData[indexPath.section][indexPath.item]
        // any other cell data configuration
        
        // this is here only during development
        c.showCellFrame = self.showCellFrame
        
        return c
    }
}

// "standard" collection view Delegate funcs
extension SampleViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Selected item at:", indexPath)
    }
}

// MARK: image data generation
class SampleData: NSObject {
    
    func generateData(_ sectionCounts: [Int]) -> [[UIImage]] {
        
        // let's generate some sample data...
        
        // we'll create numbered 200x200 UIImages,
        //  cycling through some background colors
        //  to make it easy to see the sections
        let sectionColors: [UIColor] = [
            .systemRed, .systemGreen, .systemBlue,
            .cyan, .green, .yellow,
        ]
        
        var returnArray: [[UIImage]] = []
        
        for i in 0..<sectionCounts.count {
            var sectionImages: [UIImage] = []
            let c = sectionColors[i % sectionColors.count]
            for n in 0..<sectionCounts[i] {
                if let img = createLabel(text: "\(n)", bkgColor: c) {
                    sectionImages.append(img)
                }
            }
            returnArray.append(sectionImages)
        }
        
        return returnArray
        
    }
    
    func createLabel(text: String, bkgColor: UIColor) -> UIImage? {
        let label = CATextLayer()
        let uiFont = UIFont.boldSystemFont(ofSize: 140)
        label.font = CGFont(uiFont.fontName as CFString)
        label.fontSize = 140
        label.alignmentMode = .center
        label.foregroundColor = UIColor.white.cgColor
        label.string = text
        label.shadowColor = UIColor.black.cgColor
        label.shadowOffset = .init(width: 0.0, height: 3.0)
        label.shadowRadius = 6
        label.shadowOpacity = 0.9
        
        let sz = label.preferredFrameSize()
        
        label.frame = .init(x: 0.0, y: 0.0, width: 200.0, height: sz.height)
        
        let r: CGRect = .init(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
        let renderer = UIGraphicsImageRenderer(size: r.size)
        return renderer.image { context in
            bkgColor.setFill()
            context.fill(r)
            context.cgContext.translateBy(x: 0.0, y: (200.0 - sz.height) / 2.0)
            label.render(in: context.cgContext)
        }
    }
    
}

// basic collection view cell with a
//  rounded-corners image view, 4-points "padding" on all sides
class SimpleImageCell: UICollectionViewCell {
    static let identifier: String = "simpleImageCell"
    
    let theImageView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        contentView.addSubview(theImageView)
        let g = contentView
        NSLayoutConstraint.activate([
            theImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
            theImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0),
            theImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0),
            theImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
        ])
        theImageView.layer.cornerRadius = 12
        theImageView.clipsToBounds = true
    }
    
    override var isSelected: Bool {
        didSet {
            theImageView.layer.borderWidth = isSelected ? 2.0 : 0.0
        }
    }
    
    // for development, so we can see the framing
    var showCellFrame: Bool = false {
        didSet {
            //contentView.backgroundColor = showCellFrame ? .systemYellow : .clear
            contentView.layer.borderColor = showCellFrame ? UIColor.blue.cgColor : UIColor.clear.cgColor
            contentView.layer.borderWidth = showCellFrame ? 1 : 0
        }
    }
}

class SampleGridLayout: UICollectionViewLayout {
    
    public var primaryItemSize: CGFloat = 200.0
    
    private var itemCache: [UICollectionViewLayoutAttributes] = []
    
    private var nextY: CGFloat = 0.0
    private var contentHeight: CGFloat = 0
    
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    override func prepare() {
        
        guard let collectionView = collectionView else { return }
        
        var numCols: Int = 0
        var secondarySize: CGFloat = 0
        
        if contentWidth < 450.0 {
            secondarySize = contentWidth / 4.0
            numCols = 4
        } else {
            secondarySize = primaryItemSize / 2.0
            numCols = Int(contentWidth / secondarySize)
        }
        
        var primaryFrame: CGRect = .zero
        var secondaryFrame: CGRect = .zero
        
        itemCache = []
        
        nextY = 0.0
        
        for section in 0..<collectionView.numberOfSections {
            
            let y: CGFloat = nextY
            
            var curCol: Int = 0
            var curRow: Int = 0
            
            for item in 0..<collectionView.numberOfItems(inSection: section) {
                let indexPath = IndexPath(item: item, section: section)
                
                if item == 0 {
                    
                    primaryFrame = .init(x: 0.0, y: y, width: secondarySize * 2.0, height: secondarySize * 2.0)
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                    attributes.frame = primaryFrame
                    itemCache.append(attributes)
                    
                    // item 0 takes up 2 columns
                    curCol = 2
                    
                } else {
                    
                    // if we're at the last column
                    if curCol == numCols {
                        // increment the row
                        curRow += 1
                        // if we're on row 1, next column is 2
                        //  else it's 0
                        curCol = curRow < 2 ? 2 : 0
                    }
                    
                    secondaryFrame = .init(x: CGFloat(curCol) * secondarySize, y: y + CGFloat(curRow) * secondarySize, width: secondarySize, height: secondarySize)
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                    attributes.frame = secondaryFrame
                    itemCache.append(attributes)
                    
                    // increment the column
                    curCol += 1
                    
                }
                
            }
            
            nextY = max(primaryFrame.maxY, secondaryFrame.maxY)
        }
        
        contentHeight = nextY
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        super.layoutAttributesForElements(in: rect)
        
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        
        for attributes in itemCache {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        
        return visibleLayoutAttributes
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        super.layoutAttributesForItem(at: indexPath)
        return itemCache.count > indexPath.row ? itemCache[indexPath.row] : nil
    }
    
}

相关问题