如何实现UICollectionView可折叠cell的展开/收起动画与布局更新动画同步?
问题根因
展开动画不同步的核心是:UICollectionView 执行 performBatchUpdates 时,系统只会将 cell 的 frame/center 位移变更纳入默认动画事务,若 cell 内部的布局变更(子视图约束、frame调整)没有被包裹在同一个动画事务内,就会直接跳变到最终状态,仅剩下其余 cell 的位移动画。
收起动画同步属于巧合:收起时的布局变更时机刚好落在系统动画事务的生效区间内,不具备可复现性。
最优实现方案(无hack,无需硬编码动画参数)
根据你的布局实现方式选择对应方案即可,两类方案都能实现100%和系统CollectionView动画同步。
方案1:自Sizing Cell场景(推荐)
如果你的Cell使用自动布局撑满contentView,高度由内部约束自行计算,用该方案最简单:
- Cell内部定义展开状态变量
isExpanded,状态变更时仅更新对应约束(比如展开时移除详情视图的高度为0的约束,收起时加回),调用setNeedsUpdateConstraints()触发约束更新,不要主动调用layoutIfNeeded() - 点击Cell时,仅修改对应Cell的
isExpanded状态,不需要调用reloadItems刷新Cell,直接执行空的batch更新即可:
// 点击Cell的回调逻辑 func didSelectItem(at indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? CustomExpandCell else { return } cell.isExpanded.toggle() // 空的batch更新会触发系统重新查询所有Cell的自计算高度,所有布局变更会被自动纳入同一个系统动画事务 collectionView.performBatchUpdates(nil) }
该方案完全依赖系统原生动画事务,动画时长、曲线和CollectionView默认item动画完全一致,无需任何自定义参数。
方案2:自定义Layout固定返回itemSize场景
如果你使用自定义Layout,通过layoutAttributesForItem手动返回每个Cell的高度,用该方案获取系统动画参数:
直接从当前CATransaction中读取系统预设的动画时长、时序曲线,给Cell内部动画复用,完全不需要硬编码数值:
func didSelectItem(at indexPath: IndexPath) { // 1. 更新数据源中对应条目展开状态 dataArray[indexPath.item].isExpanded.toggle() // 2. 执行batch更新 collectionView.performBatchUpdates({ // invalidate布局触发Layout重新计算itemSize collectionView.collectionViewLayout.invalidateLayout() // 从当前事务中读取系统动画参数 let animationDuration = CATransaction.animationDuration() let timingFunc = CATransaction.animationTimingFunction() // 用系统参数执行Cell内部动画 UIView.animate(withDuration: animationDuration, delay: 0, options: .init(rawValue: timingFunc?.rawValue ?? 0)) { guard let cell = collectionView.cellForItem(at: indexPath) as? CustomExpandCell else { return } cell.updateExpandStateLayout() cell.contentView.layoutIfNeeded() } }, completion: nil) }
常见错误写法排查
如果仍然不同步,检查是否存在以下操作:
- 点击Cell后先主动调用了Cell的
layoutIfNeeded(),再执行batch更新:此时Cell已经提前完成布局变更,动画事务只会作用于其余Cell的位移 - 调用
reloadItems(at:)刷新展开的Cell:Cell会被销毁重建,新Cell直接渲染为展开后的状态,无过渡动画
内容的提问来源于stack exchange,提问作者smocer
相关产品推荐
相关产品推荐

