如何实现UICollectionView单元格缩放动画与布局切换(仿iOS/谷歌相册)
iOS 相册式局部捏合缩放布局实现方案
要实现类似系统「Photos」或「Google Photos」的局部捏合缩放效果,核心是自定义 CollectionView 布局,结合捏合手势跟踪触摸中心点,动态调整每个照片项的缩放系数,同时添加过渡动画。以下是具体实现步骤和代码示例:
核心思路
- 自定义
UICollectionViewLayout,维护当前缩放系数和触摸中心点,实时计算每个 cell 的缩放变换 - 通过
UIPinchGestureRecognizer跟踪手势状态,更新缩放参数并触发布局刷新 - 基于 cell 与触摸点的距离计算衰减缩放系数,实现「近大远小」的局部缩放效果
- 手势结束后自动回弹到预设的固定缩放级别,匹配系统交互逻辑
代码实现
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
相关产品推荐
相关产品推荐

