You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为水平单行UICollectionView实现部分单元格堆叠式自定义间距?

实现带堆叠压缩效果的水平UICollectionView

这个需求我之前做过类似的场景,核心就是自定义UICollectionView的布局,让单元格数量超过指定阈值后,前X个单元格通过负间距实现重叠堆叠,后面的保持正常间距。下面给你一步步拆解实现思路和代码:

核心思路

系统的UICollectionViewFlowLayout没法直接实现这种堆叠效果,所以我们需要自定义一个布局类,重写布局计算逻辑:

  • 提前计算每个单元格的frame,当总单元格数超过设定的阈值时,对前X个单元格使用负间距,让它们相互重叠;
  • 后面的单元格依然使用正常间距,保持原有布局;
  • 正确计算collectionViewContentSize,确保滚动范围准确。

具体实现步骤

1. 自定义布局类

创建一个继承自UICollectionViewFlowLayout的子类,封装堆叠逻辑,同时暴露可配置的参数方便调整:

class StackedHorizontalLayout: UICollectionViewFlowLayout {
    // 可配置参数,根据需求调整
    var normalSpacing: CGFloat = 16               // 正常单元格间距
    var stackCount: Int = 3                      // 需要堆叠的前X个单元格数量
    var totalThreshold: Int = 5                  // 触发堆叠的总单元格阈值
    var stackOverlapRatio: CGFloat = 0.7         // 堆叠重叠比例(0~1,值越大重叠越多)
    
    private var cachedLayoutAttributes: [UICollectionViewLayoutAttributes] = []
    
    override func prepare() {
        super.prepare()
        guard let collectionView = collectionView else { return }
        cachedLayoutAttributes.removeAll()
        
        let totalItems = collectionView.numberOfItems(inSection: 0)
        let itemSize = self.itemSize
        var currentXOffset: CGFloat = sectionInset.left
        
        for itemIndex in 0..<totalItems {
            let indexPath = IndexPath(item: itemIndex, section: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            
            // 判断当前单元格是否需要参与堆叠
            let shouldStack = totalItems > totalThreshold && itemIndex < stackCount
            // 堆叠时用负间距实现重叠,正常情况用设定的间距
            let spacing = shouldStack ? -(itemSize.width * stackOverlapRatio) : normalSpacing
            
            // 设置单元格frame
            attributes.frame = CGRect(
                x: currentXOffset,
                y: sectionInset.top,
                width: itemSize.width,
                height: itemSize.height
            )
            cachedLayoutAttributes.append(attributes)
            
            // 更新下一个单元格的X偏移量(最后一个单元格不需要加间距)
            if itemIndex != totalItems - 1 {
                currentXOffset += itemSize.width + spacing
            }
        }
    }
    
    // 返回指定区域内的布局属性
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cachedLayoutAttributes.filter { $0.frame.intersects(rect) }
    }
    
    // 返回指定位置的单元格布局属性
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard indexPath.item < cachedLayoutAttributes.count else { return nil }
        return cachedLayoutAttributes[indexPath.item]
    }
    
    // 计算滚动内容的总尺寸
    override var collectionViewContentSize: CGSize {
        guard let collectionView = collectionView, !cachedLayoutAttributes.isEmpty else { return .zero }
        let lastItemFrame = cachedLayoutAttributes.last!.frame
        return CGSize(
            width: lastItemFrame.maxX + sectionInset.right,
            height: collectionView.bounds.height - sectionInset.top - sectionInset.bottom
        )
    }
}

2. 在ViewController中使用布局

配置UICollectionView并测试动态添加单元格的效果:

class DemoViewController: UIViewController {
    private var cellCount = 5
    private let stackedLayout = StackedHorizontalLayout()
    private lazy var collectionView: UICollectionView = {
        let cv = UICollectionView(frame: .zero, collectionViewLayout: stackedLayout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "StackedCell")
        cv.dataSource = self
        cv.showsHorizontalScrollIndicator = false
        cv.backgroundColor = .white
        return cv
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        configureLayout()
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        view.addSubview(collectionView)
        
        // 添加添加按钮
        let addButton = UIButton(type: .system)
        addButton.translatesAutoresizingMaskIntoConstraints = false
        addButton.setTitle("添加单元格", for: .normal)
        addButton.addTarget(self, action: #selector(addNewCell), for: .touchUpInside)
        view.addSubview(addButton)
        
        // 布局约束
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            collectionView.heightAnchor.constraint(equalToConstant: 100),
            
            addButton.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 30),
            addButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            addButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    private func configureLayout() {
        stackedLayout.scrollDirection = .horizontal
        stackedLayout.itemSize = CGSize(width: 80, height: 80)
        stackedLayout.normalSpacing = 16
        stackedLayout.stackCount = 3
        stackedLayout.totalThreshold = 5
        stackedLayout.stackOverlapRatio = 0.7 // 重叠70%,可按需调整
    }
    
    @objc private func addNewCell() {
        cellCount += 1
        // 用performBatchUpdates获得平滑的添加动画
        collectionView.performBatchUpdates {
            self.collectionView.insertItems(at: [IndexPath(item: self.cellCount - 1, section: 0)])
        } completion: { _ in
            // 可选:自动滚动到最后一个单元格
            let lastIndexPath = IndexPath(item: self.cellCount - 1, section: 0)
            self.collectionView.scrollToItem(at: lastIndexPath, at: .right, animated: true)
        }
    }
}

// MARK: - UICollectionViewDataSource
extension DemoViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return cellCount
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StackedCell", for: indexPath)
        // 给不同单元格设置不同颜色,方便区分堆叠效果
        let hue = CGFloat(indexPath.item) / CGFloat(cellCount)
        cell.backgroundColor = UIColor(hue: hue, saturation: 0.8, brightness: 0.8, alpha: 1)
        cell.layer.cornerRadius = 8
        return cell
    }
}

额外优化点

  • 堆叠方向调整:如果需要让最后X个单元格堆叠,只需要修改shouldStack的判断条件为totalItems > totalThreshold && itemIndex >= totalItems - stackCount即可;
  • 动态调整重叠比例:可以根据单元格数量动态调整重叠程度,比如数量越多重叠越多;
  • 性能优化:如果单元格数量很大,可以在prepare()方法中做缓存逻辑,避免重复计算;
  • 交互优化:如果堆叠的单元格需要响应点击,可以调整zIndex,让最上层的单元格优先响应事件。

内容的提问来源于stack exchange,提问作者DJB

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:49:23