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

如何实现UICollectionView单元格缩放动画与布局切换(仿iOS/谷歌相册)

iOS 相册式局部捏合缩放布局实现方案

要实现类似系统「Photos」或「Google Photos」的局部捏合缩放效果,核心是自定义 CollectionView 布局,结合捏合手势跟踪触摸中心点,动态调整每个照片项的缩放系数,同时添加过渡动画。以下是具体实现步骤和代码示例:

核心思路

  1. 自定义 UICollectionViewLayout,维护当前缩放系数和触摸中心点,实时计算每个 cell 的缩放变换
  2. 通过 UIPinchGestureRecognizer 跟踪手势状态,更新缩放参数并触发布局刷新
  3. 基于 cell 与触摸点的距离计算衰减缩放系数,实现「近大远小」的局部缩放效果
  4. 手势结束后自动回弹到预设的固定缩放级别,匹配系统交互逻辑

代码实现

1. 自定义局部缩放布局

import UIKit

class PinchZoomLayout: UICollectionViewFlowLayout {
    // 当前缩放系数(默认1.0)
    var currentScale: CGFloat = 1.0 {
        didSet { invalidateLayout() }
    }
    // 缩放中心点(相对于CollectionView坐标系)
    var zoomCenter: CGPoint = .zero {
        didSet { invalidateLayout() }
    }
    
    // 基础配置
    private let baseItemSize = CGSize(width: 100, height: 100)
    private let maxScale: CGFloat = 2.0
    private let minScale: CGFloat = 0.5
    
    override func prepare() {
        super.prepare()
        itemSize = baseItemSize
        minimumInteritemSpacing = 4
        minimumLineSpacing = 4
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let originAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        return originAttributes.map { adjustItemAttributes($0) }
    }
    
    private func adjustItemAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        let copiedAttributes = attributes.copy() as! UICollectionViewLayoutAttributes
        let itemCenter = copiedAttributes.center
        
        // 计算cell与触摸点的距离,生成衰减因子(距离越远缩放越小)
        let distance = hypot(itemCenter.x - zoomCenter.x, itemCenter.y - zoomCenter.y)
        let decayFactor = max(0.1, 1 - distance / (collectionView?.bounds.width ?? 375) * 0.5)
        
        // 计算最终缩放系数,限制在min/max范围内
        let finalScale = min(max(currentScale * decayFactor, minScale), maxScale)
        copiedAttributes.transform = CGAffineTransform(scaleX: finalScale, y: finalScale)
        
        // 调整层级:缩放比例大的cell在上层,避免遮挡
        copiedAttributes.zIndex = Int((maxScale - finalScale) * 1000)
        return copiedAttributes
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

2. 视图控制器集成手势与布局

import UIKit

class PhotoAlbumVC: UIViewController {
    private let collectionView: UICollectionView = {
        let layout = PinchZoomLayout()
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "photoCell")
        cv.backgroundColor = .white
        return cv
    }()
    
    private var pinchGesture: UIPinchGestureRecognizer!
    private var startScale: CGFloat = 1.0 // 手势开始时的初始缩放系数

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupGesture()
        collectionView.dataSource = self
    }
    
    private func setupUI() {
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func setupGesture() {
        pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
        collectionView.addGestureRecognizer(pinchGesture)
    }
    
    @objc private func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) {
        guard let layout = collectionView.collectionViewLayout as? PinchZoomLayout else { return }
        
        switch gesture.state {
        case .began:
            startScale = layout.currentScale
            layout.zoomCenter = gesture.location(in: collectionView)
        case .changed:
            layout.currentScale = startScale * gesture.scale
        case .ended, .cancelled:
            // 回弹到预设的固定缩放级别(0.5/1.0/1.5/2.0)
            let targetScales: [CGFloat] = [0.5, 1.0, 1.5, 2.0]
            let closestScale = targetScales.min { abs($0 - layout.currentScale) < abs($1 - layout.currentScale) } ?? 1.0
            
            // 带动画过渡到目标缩放级别
            UIView.animate(withDuration: 0.3) {
                layout.currentScale = closestScale
                self.collectionView.layoutIfNeeded()
            }
        default: break
        }
    }
}

// MARK: - UICollectionViewDataSource
extension PhotoAlbumVC: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 100
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath)
        cell.backgroundColor = .systemBlue.withAlphaComponent(CGFloat(indexPath.item % 10) / 10)
        return cell
    }
}

关键优化点

  • 坐标系转换:确保缩放中心点是相对于 CollectionView 的本地坐标系,避免因滚动偏移导致的定位错误
  • 层级管理:通过 zIndex 调整缩放后 cell 的显示层级,避免大尺寸 cell 被小尺寸 cell 遮挡
  • 性能优化:若照片数量极多,可缓存布局属性或仅计算可见区域的 cell,减少重复计算
  • 边界适配:可根据缩放后的 cell 尺寸调整 CollectionView 的 contentInset,避免内容被截断

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 17:53:12