Swift中如何让横向滚动UICollectionView滚动停止时单元格居中
横向UICollectionView滚动停止单元格自动居中实现方案
针对你当前的代码,有两种实现方案,第一种改动最小可直接适配现有逻辑,第二种复用性更强适合多场景使用。
方案一:通过UIScrollViewDelegate实现(推荐)
UICollectionView继承自UIScrollView,可直接实现滚动代理方法,在用户抬手时计算居中偏移量,体验更顺滑无跳转感。
你已经设置了collectionView.decelerationRate = .fast,完全适配轻扫切换的交互体验,只需在现有扩展中添加如下代理方法即可:
// 单个item的总步长(cell宽度 + 行间距) private var itemStepWidth: CGFloat { 320 + 15 } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // 屏蔽默认偏移计算 targetContentOffset.pointee = scrollView.contentOffset let visibleWidth = scrollView.bounds.width let cellWidth: CGFloat = 320 // 计算当前最接近的目标index let offsetCenterX = scrollView.contentOffset.x + (visibleWidth - cellWidth) / 2 var targetIndex = Int(round(offsetCenterX / itemStepWidth)) // 根据滑动速度调整index,适配快速轻扫场景 if velocity.x > 0.3 { targetIndex += 1 } else if velocity.x < -0.3 { targetIndex -= 1 } // 边界限制,避免index越界 let maxIndex = 12 - 1 targetIndex = max(0, min(maxIndex, targetIndex)) // 计算居中所需的目标偏移 let targetOffsetX = CGFloat(targetIndex) * itemStepWidth - (visibleWidth - cellWidth) / 2 scrollView.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: true) // 更新当前激活index并触发触感反馈 if currentActiveIndex != targetIndex { currentActiveIndex = targetIndex selectionFeedbackGenerator.selectionChanged() } }
配套调整建议
修改你现有insetForSectionAt方法的返回值,让首尾单元格也能正常居中:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { let sideInset = (collectionView.bounds.width - 320) / 2 return UIEdgeInsets(top: 0, left: sideInset, bottom: 0, right: sideInset) }
方案二:自定义FlowLayout实现(复用性更高)
如果需要在多个页面用到同款居中滚动效果,可以自定义UICollectionViewFlowLayout,在布局层面处理居中逻辑,无需每个页面单独实现代理方法。
自定义布局代码如下:
class CenterAlignedHorizontalFlowLayout: UICollectionViewFlowLayout { override init() { super.init() scrollDirection = .horizontal minimumLineSpacing = 15 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } let cellWidth = itemSize.width let stepWidth = cellWidth + minimumLineSpacing let offsetCenterX = collectionView.contentOffset.x + (collectionView.bounds.width - cellWidth) / 2 var targetIndex: Int switch velocity.x { case ..<(-0.3): targetIndex = Int(floor(offsetCenterX / stepWidth)) case 0.3...: targetIndex = Int(ceil(offsetCenterX / stepWidth)) default: targetIndex = Int(round(offsetCenterX / stepWidth)) } let maxIndex = collectionView.numberOfItems(inSection: 0) - 1 targetIndex = max(0, min(maxIndex, targetIndex)) let targetOffsetX = CGFloat(targetIndex) * stepWidth - (collectionView.bounds.width - cellWidth) / 2 return CGPoint(x: targetOffsetX, y: proposedContentOffset.y) } }
使用时替换你init方法中的默认FlowLayout即可:
init() { let flowLayout = CenterAlignedHorizontalFlowLayout() flowLayout.itemSize = CGSize(width: 320, height: 575) super.init(collectionViewLayout: flowLayout) }
使用该方案无需额外实现UIScrollViewDelegate的相关方法。
内容的提问来源于stack exchange,提问作者Noah Iarrobino
相关产品推荐
相关产品推荐

