輪播效果的集合視圖布局

自定義 UICollectionViewFlowLayout,用于創(chuàng)建一個 輪播效果的集合視圖布局,即類似卡片輪播(Carousel)的效果

主要功能包括:

中間卡片突出顯示:

  • 集合視圖的卡片布局中,中間的卡片被放大、完全顯示,其透明度和縮放比例也會被設(shè)置為較高值。
側(cè)邊卡片縮小或部分顯示:
  • 中心卡片的兩側(cè)卡片會按一定的比例縮小,同時可以設(shè)置透明度降低和偏移位置。
流暢的滑動與自動對齊:
  • 用戶在滾動時,松手后會自動對齊最近的卡片,使其成為中心可見的卡片。
支持水平或垂直方向滾動:
  • 滾動方向可以是水平或垂直,并且可以根據(jù)需要調(diào)整卡片的排列方式。
自定義間距與顯示模式:
  • 通過 CarouselFlowLayoutSpacingMode 提供兩種間距模式:
    • fixed: 固定的卡片間距。
    • overlap: 重疊模式,可調(diào)整可見的偏移量。

\color{Red} {\underline{\mathbf{collectionView 在使用時,不能設(shè)置 isPagingEnabled 為 True}}}

// 定義一個枚舉,用于設(shè)置滾動布局的間距模式
public enum CarouselFlowLayoutSpacingMode {
    /// 每個卡片之間設(shè)置一個固定的間距
    case fixed(spacing: CGFloat)
    
    /// 卡片之間的重疊效果,通過 visibleOffset 控制側(cè)邊卡片的可見部分
    case overlap(visibleOffset: CGFloat)
}

// 定義一個輪播滾動布局類,繼承自 UICollectionViewFlowLayout
open class CarouselFlowLayout: UICollectionViewFlowLayout {
    
    // 內(nèi)部結(jié)構(gòu)體,用于記錄布局狀態(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
        }
    }
    
    // 屬性:側(cè)邊項的縮放比例
    @IBInspectable open var sideItemScale: CGFloat = 0.9
    // 屬性:側(cè)邊項的透明度
    @IBInspectable open var sideItemAlpha: CGFloat = 1.0
    // 屬性:側(cè)邊項的偏移量
    @IBInspectable open var sideItemShift: CGFloat = 0.0
    // 間距模式,默認(rèn)為固定間距
    open var spacingMode = CarouselFlowLayoutSpacingMode.fixed(spacing: 5)
    
    // 當(dāng)前布局狀態(tài)
    fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
    
    // 準(zhǔn)備布局的方法,每次布局改變都會調(diào)用
    override open func prepare() {
        super.prepare()
        // 獲取當(dāng)前集合視圖的布局狀態(tài)
        let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)
        
        // 如果布局狀態(tài)發(fā)生變化,則重新設(shè)置集合視圖并更新布局
        if !self.state.isEqual(currentState) {
            self.setupCollectionView()
            self.updateLayout()
            self.state = currentState
        }
    }
    
    // 設(shè)置集合視圖的一些基本屬性
    fileprivate func setupCollectionView() {
        guard let collectionView = self.collectionView else { return }
        // 設(shè)置集合視圖的減速率為快速
        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ù)間距模式設(shè)置最小行間距
        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) })
    }
    
    // 對單元格的布局屬性進行變換(縮放、透明度和位置調(diào)整)
    fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        guard let collectionView = self.collectionView else { return attributes }
        let isHorizontal = (self.scrollDirection == .horizontal) // 是否水平滾動
        
        // 集合視圖中心點的坐標(biāo)
        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
        
        // 計算最大距離和當(dāng)前距離
        let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
        let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
        let ratio = (maxDistance - distance) / maxDistance
        
        // 設(shè)置透明度、縮放比例和偏移量
        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ù)滾動方向調(diào)整單元格的位置
        if isHorizontal {
            attributes.center.y = attributes.center.y + shift
        } else {
            attributes.center.x = attributes.center.x + shift
        }
        
        return attributes
    }
    
    // 確定目標(biāo)內(nèi)容偏移量,用于滾動停止時對齊單元格
    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) // 是否水平滾動
        
        // 集合視圖中心點的坐標(biāo)
        let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
        // 目標(biāo)內(nèi)容偏移量的中心點
        let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
        
        var targetContentOffset: CGPoint
        if isHorizontal {
            // 找到距離目標(biāo)偏移中心最近的單元格
            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
    }
}

計算當(dāng)前頁的索引

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    // 將當(dāng)前集合視圖的布局轉(zhuǎn)換為自定義的 CarouselFlowLayout
    let layout = self.collectionView.collectionViewLayout as! CarouselFlowLayout
    
    // 計算每頁的寬度(包括每個項目的寬度和行間距)
    let pageSide = layout.itemSize.width + layout.minimumLineSpacing
    
    // 獲取當(dāng)前滾動視圖的水平偏移量
    let offset = scrollView.contentOffset.x
    
    /**
     根據(jù)偏移量計算當(dāng)前頁的索引
     計算邏輯:
     1. 將當(dāng)前偏移量減去頁面寬度的一半以確保正確的頁對齊
     2. 將結(jié)果除以每頁寬度以得到精確的位置索引
     3. 使用 floor 函數(shù)向下取整以確保整數(shù)索引
     4. 加 1 是為了將偏移量對齊到從 0 開始的索引
     */
    let index = Int(floor((offset - pageSide / 2) / pageSide) + 1)
    
    // 打印當(dāng)前的頁索引,用于調(diào)試或記錄
    PrintLog(message: index)
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容