自定義 UICollectionViewFlowLayout,用于創(chuàng)建一個 輪播效果的集合視圖布局,即類似卡片輪播(Carousel)的效果
主要功能包括:
中間卡片突出顯示:
- 集合視圖的卡片布局中,中間的卡片被放大恼五、完全顯示镰矿,其透明度和縮放比例也會被設置為較高值径簿。
側邊卡片縮小或部分顯示:
- 中心卡片的兩側卡片會按一定的比例縮小罢屈,同時可以設置透明度降低和偏移位置。
流暢的滑動與自動對齊:
- 用戶在滾動時篇亭,松手后會自動對齊最近的卡片缠捌,使其成為中心可見的卡片。
支持水平或垂直方向滾動:
- 滾動方向可以是水平或垂直译蒂,并且可以根據(jù)需要調整卡片的排列方式曼月。
自定義間距與顯示模式:
- 通過 CarouselFlowLayoutSpacingMode 提供兩種間距模式:
- fixed: 固定的卡片間距。
- overlap: 重疊模式柔昼,可調整可見的偏移量哑芹。
// 定義一個枚舉捕透,用于設置滾動布局的間距模式
public enum CarouselFlowLayoutSpacingMode {
/// 每個卡片之間設置一個固定的間距
case fixed(spacing: CGFloat)
/// 卡片之間的重疊效果聪姿,通過 visibleOffset 控制側邊卡片的可見部分
case overlap(visibleOffset: CGFloat)
}
// 定義一個輪播滾動布局類,繼承自 UICollectionViewFlowLayout
open class CarouselFlowLayout: UICollectionViewFlowLayout {
// 內部結構體乙嘀,用于記錄布局狀態(tài)
fileprivate struct LayoutState {
var size: CGSize // 集合視圖的尺寸
var direction: UICollectionView.ScrollDirection // 滾動方向
// 判斷兩個布局狀態(tài)是否相等
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}
// 屬性:側邊項的縮放比例
@IBInspectable open var sideItemScale: CGFloat = 0.9
// 屬性:側邊項的透明度
@IBInspectable open var sideItemAlpha: CGFloat = 1.0
// 屬性:側邊項的偏移量
@IBInspectable open var sideItemShift: CGFloat = 0.0
// 間距模式末购,默認為固定間距
open var spacingMode = CarouselFlowLayoutSpacingMode.fixed(spacing: 5)
// 當前布局狀態(tài)
fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
// 準備布局的方法,每次布局改變都會調用
override open func prepare() {
super.prepare()
// 獲取當前集合視圖的布局狀態(tài)
let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)
// 如果布局狀態(tài)發(fā)生變化乒躺,則重新設置集合視圖并更新布局
if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}
// 設置集合視圖的一些基本屬性
fileprivate func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
// 設置集合視圖的減速率為快速
if collectionView.decelerationRate != UIScrollView.DecelerationRate.fast {
collectionView.decelerationRate = UIScrollView.DecelerationRate.fast
}
}
// 更新布局
fileprivate func updateLayout() {
guard let collectionView = self.collectionView else { return }
let collectionSize = collectionView.bounds.size // 集合視圖的尺寸
let isHorizontal = (self.scrollDirection == .horizontal) // 是否水平滾動
// 計算上下和左右的邊距招盲,使單元格居中
let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsets.init(top: yInset, left: xInset, bottom: yInset, right: xInset)
// 計算縮放后的單元格偏移量
let side = isHorizontal ? self.itemSize.width : self.itemSize.height
let scaledItemOffset = (side - side * self.sideItemScale) / 2
// 根據(jù)間距模式設置最小行間距
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}
// 在集合視圖的邊界發(fā)生變化時,是否需要重新計算布局
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// 返回布局中所有可見單元格的布局屬性
override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
// 修改每個布局屬性并返回
return attributes.map({ self.transformLayoutAttributes($0) })
}
// 對單元格的布局屬性進行變換(縮放嘉冒、透明度和位置調整)
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal) // 是否水平滾動
// 集合視圖中心點的坐標
let collectionCenter = isHorizontal ? collectionView.frame.size.width / 2 : collectionView.frame.size.height / 2
// 滾動偏移量
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
// 單元格中心點相對于集合視圖中心的偏移量
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset
// 計算最大距離和當前距離
let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance) / maxDistance
// 設置透明度曹货、縮放比例和偏移量
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
let shift = (1 - ratio) * self.sideItemShift
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
// 根據(jù)滾動方向調整單元格的位置
if isHorizontal {
attributes.center.y = attributes.center.y + shift
} else {
attributes.center.x = attributes.center.x + shift
}
return attributes
}
// 確定目標內容偏移量,用于滾動停止時對齊單元格
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView, !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let isHorizontal = (self.scrollDirection == .horizontal) // 是否水平滾動
// 集合視圖中心點的坐標
let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
// 目標內容偏移量的中心點
let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
var targetContentOffset: CGPoint
if isHorizontal {
// 找到距離目標偏移中心最近的單元格
let closest = layoutAttributes.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
} else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}
return targetContentOffset
}
}
計算當前頁的索引
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// 將當前集合視圖的布局轉換為自定義的 CarouselFlowLayout
let layout = self.collectionView.collectionViewLayout as! CarouselFlowLayout
// 計算每頁的寬度(包括每個項目的寬度和行間距)
let pageSide = layout.itemSize.width + layout.minimumLineSpacing
// 獲取當前滾動視圖的水平偏移量
let offset = scrollView.contentOffset.x
/**
根據(jù)偏移量計算當前頁的索引
計算邏輯:
1. 將當前偏移量減去頁面寬度的一半以確保正確的頁對齊
2. 將結果除以每頁寬度以得到精確的位置索引
3. 使用 floor 函數(shù)向下取整以確保整數(shù)索引
4. 加 1 是為了將偏移量對齊到從 0 開始的索引
*/
let index = Int(floor((offset - pageSide / 2) / pageSide) + 1)
// 打印當前的頁索引讳推,用于調試或記錄
PrintLog(message: index)
}