輪播效果的集合視圖布局

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

主要功能包括:

中間卡片突出顯示:

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

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

// 定義一個枚舉捕透,用于設置滾動布局的間距模式
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)
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末顶籽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子银觅,更是在濱河造成了極大的恐慌礼饱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件究驴,死亡現(xiàn)場離奇詭異镊绪,居然都是意外死亡,警方通過查閱死者的電腦和手機洒忧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門蝴韭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人熙侍,你說我怎么就攤上這事榄鉴÷哪ィ” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵庆尘,是天一觀的道長剃诅。 經(jīng)常有香客問我,道長驶忌,這世上最難降的妖魔是什么矛辕? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮位岔,結果婚禮上如筛,老公的妹妹穿的比我還像新娘。我一直安慰自己抒抬,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布晤柄。 她就那樣靜靜地躺著擦剑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芥颈。 梳的紋絲不亂的頭發(fā)上惠勒,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音爬坑,去河邊找鬼纠屋。 笑死,一個胖子當著我的面吹牛盾计,可吹牛的內容都是我干的售担。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼署辉,長吁一口氣:“原來是場噩夢啊……” “哼族铆!你這毒婦竟也來了?” 一聲冷哼從身側響起哭尝,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哥攘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后材鹦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逝淹,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年桶唐,在試婚紗的時候發(fā)現(xiàn)自己被綠了栅葡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡莽红,死狀恐怖妥畏,靈堂內的尸體忽然破棺而出邦邦,到底是詐尸還是另有隱情,我是刑警寧澤醉蚁,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布燃辖,位于F島的核電站,受9級特大地震影響网棍,放射性物質發(fā)生泄漏黔龟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一滥玷、第九天 我趴在偏房一處隱蔽的房頂上張望氏身。 院中可真熱鬧,春花似錦惑畴、人聲如沸蛋欣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陷虎。三九已至,卻和暖如春杠袱,著一層夾襖步出監(jiān)牢的瞬間尚猿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工楣富, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凿掂,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓纹蝴,卻偏偏與公主長得像庄萎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骗灶,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內容