UICollectionViewLayout 全套源码 + 逐行中文注释 + 使用场景说明

这份就是自定义 UICollectionViewLayout全部核心 API 头文件定义 ; 你继承 UICollectionViewLayout 写瀑布流、卡片、圆环、圆形布局、翻页效果、复杂不规则网格,全部重写这里面的方法。

swift 复制代码
import CoreGraphics
import Foundation
import UIKit

extension UICollectionView {

    /// 表头视图 标记字符串
    @available(iOS 6.0, *)
    public class let elementKindSectionHeader: String

    /// 表尾视图 标记字符串
    @available(iOS 6.0, *)
    public class let elementKindSectionFooter: String

    /// 滚动方向
    public enum ScrollDirection : Int, @unchecked Sendable {
        case vertical = 0      // 垂直滚动(上下滑),默认
        case horizontal = 1    // 水平滚动(左右滑)
    }

    /// 布局元素类型枚举
    public enum ElementCategory : UInt, @unchecked Sendable {
        case cell = 0                     // 正常内容Cell单元格
        case supplementaryView = 1        // 追加视图:Header / Footer
        case decorationView = 2           // 装饰视图:背景、分割线,不绑定数据源
    }
}

// MARK: - 布局属性:决定每一个Cell/Header/Footer/装饰View的位置、大小、形变、透明度
/// 场景:自定义布局的核心数据模型。
/// 每一个视图都对应一份LayoutAttributes,布局计算就是生成一堆该对象。
@available(iOS 6.0, *)
@MainActor open class UICollectionViewLayoutAttributes : NSObject, NSCopying, UIDynamicItem {

    /// 视图的frame(原点+宽高)
    open var frame: CGRect
    /// 中心点坐标
    open var center: CGPoint
    /// 尺寸大小
    open var size: CGSize
    /// 3D形变 CATransform3D,可做透视、旋转3D效果
    open var transform3D: CATransform3D

    /// iOS7+ bounds,相对于自身坐标系
    @available(iOS 7.0, *)
    open var bounds: CGRect

    /// iOS7+ 2D仿射变换:缩放、旋转、平移
    @available(iOS 7.0, *)
    open var transform: CGAffineTransform

    /// 透明度 0~1
    open var alpha: CGFloat

    /// 层级,数值越大越在上层,可以实现视图重叠效果
    open var zIndex: Int

    /// 是否隐藏
    open var isHidden: Bool

    /// 当前元素对应的索引位置
    open var indexPath: IndexPath

    /// 只读:当前元素类型 Cell / supplementary / decoration
    open var representedElementCategory: UICollectionView.ElementCategory { get }

    /// 只读:追加视图标记字符串,Header/Footer;Cell返回nil
    open var representedElementKind: String? { get }

    /// 创建Cell单元格布局属性
    public convenience init(forCellWith indexPath: IndexPath)

    /// 创建Header/Footer追加视图布局属性
    public convenience init(forSupplementaryViewOfKind elementKind: String, with indexPath: IndexPath)

    /// 创建装饰视图布局属性(背景、分割线等)
    public convenience init(forDecorationViewOfKind decorationViewKind: String, with indexPath: IndexPath)
}

// MARK: - 布局失效上下文:实现**局部刷新布局**,优化性能,不要全量重绘
/// 场景1:滚动时只刷新可视区域Item;
/// 场景2:某几个Cell高度变化,只刷新这几个,不用invalidateLayout()全部刷新;
/// 场景3:自定义动画、交互式拖拽移动Item。
@available(iOS 7.0, *)
@MainActor open class UICollectionViewLayoutInvalidationContext : NSObject {

    /// 是否全部布局失效,true=所有布局全部重新计算
    open var invalidateEverything: Bool { get }

    /// 是否数据源数量发生变化(增删Cell),需要重新计算
    open var invalidateDataSourceCounts: Bool { get }

    /// iOS8+ 标记指定若干个Item需要重新布局
    @available(iOS 8.0, *)
    open func invalidateItems(at indexPaths: [IndexPath])

    /// iOS8+ 刷新指定Header/Footer追加视图
    @available(iOS 8.0, *)
    open func invalidateSupplementaryElements(ofKind elementKind: String, at indexPaths: [IndexPath])

    /// iOS8+ 刷新指定装饰视图
    @available(iOS 8.0, *)
    open func invalidateDecorationElements(ofKind elementKind: String, at indexPaths: [IndexPath])

    /// 获取本次需要刷新的cell索引数组
    @available(iOS 8.0, *)
    open var invalidatedItemIndexPaths: [IndexPath]? { get }

    /// 获取本次刷新的追加视图字典 [kind字符串 : 索引数组]
    @available(iOS 8.0, *)
    open var invalidatedSupplementaryIndexPaths: [String : [IndexPath]]? { get }

    /// 获取本次刷新的装饰视图
    @available(iOS 8.0, *)
    open var invalidatedDecorationIndexPaths: [String : [IndexPath]]? { get }

    /// iOS8+ 滚动偏移量调整;滚动过程布局变化,修正contentOffset防止跳动
    @available(iOS 8.0, *)
    open var contentOffsetAdjustment: CGPoint

    /// iOS8+ 内容总大小调整
    @available(iOS 8.0, *)
    open var contentSizeAdjustment: CGSize

    /// iOS9+ 交互式移动Item:移动前索引数组
    @available(iOS 9.0, *)
    open var previousIndexPathsForInteractivelyMovingItems: [IndexPath]? { get }

    /// iOS9+ 交互式移动Item:目标位置索引数组
    @available(iOS 9.0, *)
    open var targetIndexPathsForInteractivelyMovingItems: [IndexPath]? { get }

    /// iOS9+ 拖拽移动手势的触点坐标
    @available(iOS 9.0, *)
    open var interactiveMovementTarget: CGPoint { get }
}

// MARK: - 布局基类 UICollectionViewLayout 主入口
/// 场景:所有自定义布局的父类;瀑布流、卡片轮播、环形布局、不规则网格都继承它
@available(iOS 6.0, *)
@MainActor open class UICollectionViewLayout : NSObject, NSCoding {

    public init()

    public init?(coder: NSCoder)

    /// 弱引用绑定的collectionView,只读
    open var collectionView: UICollectionView? { get }

    /// 使整个布局全部失效,触发重新布局计算;性能差,会刷新全部
    open func invalidateLayout()

    /// iOS7+ 带上下文的布局失效,推荐局部刷新;高性能,精准刷新部分Item
    @available(iOS 7.0, *)
    open func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext)

    /// 注册装饰视图Class,背景/分割线等
    open func register(_ viewClass: AnyClass?, forDecorationViewOfKind elementKind: String)

    /// 通过Xib注册装饰视图
    open func register(_ nib: UINib?, forDecorationViewOfKind elementKind: String)
}

// MARK: - 【核心布局计算回调】最常重写的一批方法
extension UICollectionViewLayout {

    /// 返回自定义的LayoutAttributes子类类型;如果你继承UICollectionViewLayoutAttributes扩展字段就重写
    open class var layoutAttributesClass: AnyClass { get }

    /// iOS7+ 返回自定义的失效上下文类型;自定义局部刷新逻辑重写
    @available(iOS 7.0, *)
    open class var invalidationContextClass: AnyClass { get }

    /// ⭐布局计算第一步!所有布局计算在这里写。
    /// 场景:一次性预计算全部Cell、Header、Footer的frame;瀑布流高度缓存就在这里。
    open func prepare()

    /// ⭐核心回调:返回rect可视范围内所有元素的布局属性。CollectionView滚动时高频调用。
    open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

    /// 获取单个Cell对应的布局属性
    open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// 获取Header/Footer追加视图布局属性
    open func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// 获取装饰视图布局属性
    open func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// ⭐边界变化(滚动、缩放)时是否需要刷新布局;
    /// 场景:卡片轮播,滚动实时形变,返回true;普通瀑布流返回false。
    open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool

    /// iOS7+ 滚动边界变化时,返回自定义的失效上下文对象
    @available(iOS 7.0, *)
    open func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext

    /// iOS8+ Cell自适应宽高(Self‑Sizing),系统预估尺寸和最终实际尺寸不一样时是否刷新布局
    @available(iOS 8.0, *)
    open func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool

    /// iOS8+ 自适应尺寸布局失效上下文
    @available(iOS 8.0, *)
    open func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext

    /// ⭐非常高频!松手滚动减速时,修正最终停留位置;实现分页、卡片居中停靠效果必重写!
    open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint

    /// iOS7+ 无速度参数的目标偏移回调
    @available(iOS 7.0, *)
    open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint

    /// ⭐只读属性,返回CollectionView整体内容总大小;类似UIScrollView contentSize,必须重写!
    open var collectionViewContentSize: CGSize { get }

    /// 获取布局方向 LTR / RTL
    open var developmentLayoutDirection: UIUserInterfaceLayoutDirection { get }

    /// 是否在从右向左语言下,水平布局自动翻转
    open var flipsHorizontallyInOppositeLayoutDirection: Bool { get }
}

// MARK: - 【增删动画过渡回调】Cell插入/删除动画、布局切换动画
/// 场景:自定义删除渐隐、飞入出场动画;两个Layout互相切换转场动画
extension UICollectionViewLayout {

    /// 开始执行批量增删更新动画前回调
    open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem])

    /// 批量增删动画全部完成后回调
    open func finalizeCollectionViewUpdates()

    /// bounds即将发生变化动画之前
    open func prepare(forAnimatedBoundsChange oldBounds: CGRect)

    /// bounds动画结束
    open func finalizeAnimatedBoundsChange()

    /// iOS7+ 切换到新布局之前(转场动画)
    @available(iOS 7.0, *)
    open func prepareForTransition(to newLayout: UICollectionViewLayout)

    /// iOS7+ 从旧布局切换出来
    @available(iOS 7.0, *)
    open func prepareForTransition(from oldLayout: UICollectionViewLayout)

    /// iOS7+ 布局切换动画完成
    @available(iOS 7.0, *)
    open func finalizeLayoutTransition()

    /// 即将出现的Item,入场动画起始位置属性;实现飞入效果
    open func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// 即将消失的Item,退场动画结束位置属性;实现消失动画
    open func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// Header/Footer入场动画初始属性
    open func initialLayoutAttributesForAppearingSupplementaryElement(ofKind elementKind: String, at elementIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// Header/Footer退场结束属性
    open func finalLayoutAttributesForDisappearingSupplementaryElement(ofKind elementKind: String, at elementIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// 装饰视图入场动画
    open func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// 装饰视图退场动画
    open func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?

    /// iOS7+ 返回需要删除的追加视图索引数组
    @available(iOS 7.0, *)
    open func indexPathsToDeleteForSupplementaryView(ofKind elementKind: String) -> [IndexPath]

    /// iOS7+ 返回需要删除的装饰视图索引数组
    @available(iOS 7.0, *)
    open func indexPathsToDeleteForDecorationView(ofKind elementKind: String) -> [IndexPath]

    /// iOS7+ 返回需要插入追加视图索引数组
    @available(iOS 7.0, *)
    open func indexPathsToInsertForSupplementaryView(ofKind elementKind: String) -> [IndexPath]

    /// iOS7+ 返回需要插入装饰视图索引数组
    @available(iOS 7.0, *)
    open func indexPathsToInsertForDecorationView(ofKind elementKind: String) -> [IndexPath]
}

// MARK: - 【iOS9+ 交互式拖拽移动Item】长按拖拽排序功能
extension UICollectionViewLayout {

    /// iOS9+ 根据拖拽手势坐标,算出目标移动到哪一个IndexPath
    @available(iOS 9.0, *)
    open func targetIndexPath(forInteractivelyMovingItem previousIndexPath: IndexPath, withPosition position: CGPoint) -> IndexPath

    /// iOS9+ 拖拽过程中,被拖拽Item实时布局属性(跟随手指移动)
    @available(iOS 9.0, *)
    open func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath, withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes

    /// iOS9+ 拖拽移动开始,生成失效上下文
    @available(iOS 9.0, *)
    open func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath], withTargetPosition targetPosition: CGPoint, previousIndexPaths: [IndexPath], previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext

    /// iOS9+ 拖拽手势结束;movementCancelled=true代表拖拽取消,不换位
    @available(iOS 9.0, *)
    open func invalidationContextForEndingInteractiveMovementOfItems(toFinalIndexPaths indexPaths: [IndexPath], previousIndexPaths: [IndexPath], movementCancelled: Bool) -> UICollectionViewLayoutInvalidationContext
}

自定义布局开发最简必重写清单(80% 场景只需要这 4 个)

表格

方法 作用 使用场景
prepare() 预计算所有 item 位置,缓存 frame 数组 瀑布流、环形布局、不规则网格
collectionViewContentSize 返回整体滚动内容宽高 所有自定义布局必须实现
layoutAttributesForElements(in rect:) 返回可视区域 item 布局 滚动实时渲染
targetContentOffset(...) 松手停靠偏移修正 卡片轮播、分页居中效果

高级场景对应 API

  1. 滚动时 Cell 缩放、倾斜动画

    • shouldInvalidateLayout(forBoundsChange:) 返回 true
  2. 高性能局部刷新,避免全量重绘卡顿

    • invalidateLayout(with:) + UICollectionViewLayoutInvalidationContext
  3. Cell 增删自定义出场入场动画

    • initialLayoutAttributesForAppearingItem / finalLayoutAttributesForDisappearingItem
  4. 长按拖拽排序

    • iOS9+ 交互式移动 Item 一套方法
  5. 布局切换动画 (A 布局→B 布局带动画)

    • prepareForTransition 系列转场回调
  6. 背景装饰视图、网格分割线

    • DecorationView 装饰视图 + register 注册
相关推荐
铁皮饭盒4 小时前
浏览器AI肯定离不开onnxruntime, 微软开源
前端·javascript·后端
码视野4 小时前
基于 Vue3 + Element Plus 的【智慧社区洗车养车与上门流动洗车预约调度系统】设计与实现(含PRD/三端源码/大屏)
前端·javascript·vue.js·人工智能·vue3
码视野4 小时前
基于 Vue3 + Element Plus 的【无人值守共享自习室与空间智能预约系统】设计与实现(含PRD/三端源码/大屏)
前端·javascript·vue.js·人工智能·vue3
CarIise4 小时前
JavaScript基础语法与DOM操作实战课堂笔记
开发语言·javascript·笔记
Su米苏5 小时前
针对使用@vue-office 出现的包异常找不到问题
前端·javascript·vue.js
坚定信念,勇往无前5 小时前
electron打包mac ,apple m4出错:ffprobe是x86,不是arm64
前端·javascript·electron
专业抄代码选手5 小时前
01|从手写 DOM 到虚拟 DOM:把 UI 从“操作”变成“描述”
前端·javascript·react.js
专业抄代码选手5 小时前
03|React 为什么需要协调:比较两棵树,而不是重建页面
前端·javascript·react.js
王霸天5 小时前
GLB 文件太大怎么办?我把它从 47MB 压到 8MB,全程没碰命令行
前端·javascript·程序员