自定义UICollectionView布局实战:CardsAgainst白卡滑动渐隐效果的实现原理
【免费下载链接】CardsAgainstAn iOS game for horrible people项目地址: https://gitcode.com/gh_mirrors/ca/CardsAgainst
CardsAgainst 是一款用 Swift 编写的 iOS 卡牌对战游戏(An iOS game for horrible people),它基于 Multipeer Connectivity 实现局域网内的多人组局。除了联机能力,这个项目还有一个值得学习的小亮点:自定义 UICollectionView 布局——白卡列表在向上滑动时,卡片会从屏幕顶部以"渐隐"方式淡入,而不是生硬地出现。今天我们就用通俗的方式拆解这个滑动渐隐效果的实现原理,带你看懂 iOS 自定义布局的核心套路。
一、先认识这个"白卡渐隐"效果
游戏进行到答题环节时,玩家会看到一张黑卡(题目)和下方一叠白卡(选项)。当白卡数量较多、需要上下滑动浏览时,新滑入视野的卡片并不是直接"蹦"出来的,而是从顶部透明处缓缓变淡入变深,整个过程只有约 120 点的过渡区域,配合缓动曲线,视觉上非常柔和。
这个效果的全部代码量不到 50 行,全部集中在一个文件里:
- 布局核心:
CardsAgainst/Controllers/WhiteCardFlowLayout.swift - 卡片外观:
CardsAgainst/Views/WhiteCardCell.swift - 列表集成与滚动逻辑:
CardsAgainst/View Controllers/GameViewController.swift
二、实现自定义 UICollectionView 布局的两大抓手
在 UIKit 中做自定义布局,最常见的起点是继承UICollectionViewFlowLayout(流式布局),然后只重写你关心的两个方法。CardsAgainst 正是这样做的:
1. 重写layoutAttributesForElements(in:):给每张卡"算透明度"
这个方法负责返回可见区域内所有单元格的布局属性。WhiteCardFlowLayout先调用父类拿到标准布局结果,然后对每张卡做一次判断:
- 把卡片的
y坐标换算到父视图坐标系(collectionView.convert(...)); - 如果卡片位置仍藏在顶部内容留白(contentInset)之内,说明它"还藏在屏幕上方";
- 计算它距离露出的程度
progress(过渡区域固定为 120 点),再用自定义缓动函数换算成alpha,范围从 0.05 到 1:
let difference = topContentInset - yOriginInSuperview let progress = difference / transitionRegion attributes.alpha = easeInOut(min(progress, 1), b: 1, c: -0.95, d: 1)也就是说:藏得越深,越透明;滑得越多,越不透明。这就是"渐隐"的本质——不是动画,而是每帧根据位置实时计算透明度。
2. 重写shouldInvalidateLayout(forBoundsChange:):让布局"跟着手指走"
这是最容易被忽略、却最关键的一行:
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }它告诉系统:滚动(bounds 变化)时也要重新计算布局。因为layoutAttributesForElements里的透明度依赖滚动位置,只有持续重算,卡片才会随手指实时淡入淡出。默认流式布局会在内容不变时跳过重算以省性能,而这里我们主动要求"每次都算"——好在只是简单数学运算,代价极低。
三、渐隐效果的灵魂:手写 easeInOut 缓动函数
线性淡入会显得机械,好的渐隐需要一个"先慢、再快、再慢"的节奏。WhiteCardFlowLayout.swift顶部自己实现了一个经典的三次缓动函数easeInOut:
- 进度
t在前半程走t*t*t,起步更缓; - 后半程对称加速收尾。
把progress喂给这个函数,得到的alpha曲线就比线性插值自然得多。这个小函数虽然只有几行,却是很多商业 App 中"手感好"的秘密之一。
四、在游戏主界面中的集成方式
效果最终如何落地?看GameViewController.swift的集成方式(约第 34 行起):
- 创建
UICollectionView时直接传入WhiteCardFlowLayout()作为布局对象,零额外配置; - 通过
contentInset.top把黑卡题目区域的空间让给列表——注意:渐隐过渡的"顶部"正是这个 contentInset 区域,所以布局代码里用的是collectionView.contentInset.top + 20; - 每次补充新卡、删除已选卡时,用
performBatchUpdates配合reloadData()刷新,滚动后列表会scrollRectToVisible回顶,让渐隐效果在每一轮都自然重演; - 白卡单元格
WhiteCardCell本身只负责外观:圆角 8 点的浅色背景 + 自动换行的 UILabel,用 Cartography 约束文字边距。
这种"布局归布局、外观归 Cell、时机归 ViewController"的分工,正是自定义布局项目的清晰架构。
五、新手抄作业清单:5 个关键点
- 继承
UICollectionViewFlowLayout,只重写需要的钩子方法,不要从零写UICollectionViewLayout; - 用
convert(_:to:)统一坐标系再比较位置,否则 contentInset、嵌套滚动会让坐标计算全乱; - 过渡区域(transitionRegion)是个魔法数字,按视觉调整,本例是 120 点;
- 记得重写
shouldInvalidateLayout(forBoundsChange:)返回true,否则效果只在首次加载时出现一次; min(progress, 1)做截断,避免卡片滑出区域后progress越界算出异常透明度。
掌握这套"位置 → 进度 → 缓动 → alpha"的公式,你完全可以把它迁移到自己的项目里:商品列表的淡入、新闻流的渐显、卡片堆叠的入场动画,都是同一个原理的变体。想进一步学习,可以直接阅读 WhiteCardFlowLayout.swift、WhiteCardCell.swift 与 GameViewController.swift 这三个文件,全部核心逻辑一目了然。
【免费下载链接】CardsAgainstAn iOS game for horrible people项目地址: https://gitcode.com/gh_mirrors/ca/CardsAgainst
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考