Swift - UICollectionView 使用2

整理一下UICollectionView的使用實現(xiàn)項目中的某些效果步责。

效果圖如下:


效果圖.png

實現(xiàn)以上效果返顺,并且可以無限循環(huán),或者不無限循環(huán)蔓肯。
除了以上效果外遂鹊,還可以自由搭配其他效果,這里就不多說了蔗包,可以自己試試秉扑。


我們先來了解一下居中顯示核心方法,如下:

// 通過indexPath將視圖滑動到你指定的item调限,并且可以設(shè)置該item在屏幕中顯示的位置(橫向:左中右舟陆;豎向:上中下)
open func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionViewScrollPosition, animated: Bool)

下面讓我們了解一下思路:

  1. 看到這種效果我們應(yīng)該首先會想到使用UICollectionView,因為它可以自定義布局耻矮,橫向滑動也比較方便設(shè)置等秦躯;
  2. 決定使用什么控件實現(xiàn)需求中的效果,我們要開始思考效果中我們都需要對哪些進(jìn)行設(shè)置裆装,也就是UICollectionViewFlowLayout相關(guān)屬性的設(shè)置踱承。從圖中我們可以想到要設(shè)置itemSize、item之間的間距米母、item與屏幕之間的間距勾扭、橫向華動等;
  3. 新建一個CarouselView和CarouselCollectionCell铁瞒,根據(jù)需求初始化控件妙色,這里只是一張本地圖片;
  4. 準(zhǔn)備的差不多了慧耍,就開始編寫代碼吧身辨,將要用到的東西,定義并初始化實現(xiàn)等等芍碧;
  5. 邊編寫代碼變運(yùn)行看看效果煌珊,不至于編寫完效果不對還不好找原因;
  6. 大體效果出來后泌豆,我們考慮一下如何讓每個item都顯示在中間定庵,這里就用到了我們上面提到的方法了;
  7. 我們需要讓它以page的方式滑動(這里的page和UICollectionView的isPagingEnabled屬性是兩回事),這里我們需要使用UIScrollViewDelegate中的兩個方法蔬浙,一個是開始拖拽的方法scrollViewWillBeginDragging猪落,另一個是結(jié)束拖拽方法scrollViewDidEndDragging,通過這兩個方法記錄x坐標(biāo)值畴博,計算出我們要顯示的item笨忌,并且居中顯示。那可能有人會問俱病,為什么不直接設(shè)置UICollectionView的isPagingEnabled屬性官疲。因為我們的item并不是占屏幕的全部寬度,也就是除了當(dāng)前的item還要加上兩側(cè)的item的一部分才是isPagingEnabled的整個屏幕寬亮隙,可想而知滑動后的效果并不是我們想要的那種了途凫,可以自己實驗一把;
  8. 封裝溢吻、優(yōu)化代碼以便外部調(diào)用少量代碼實現(xiàn)該效果颖榜,或其他地方使用。



開始上代碼:(這里省略cell的實現(xiàn)煤裙,可根據(jù)各自需求實現(xiàn))

以下代碼都是在 CarouselView 中
定義協(xié)議留待內(nèi)部調(diào)用、外部使用
/**
 *  cell相關(guān)協(xié)議方法
 **/
@objc protocol CarouselViewDelegate: NSObjectProtocol {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
    @objc optional func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
}
定義需要的變量噪漾,并做一些初始化
    /*
     *  MARK: - 定義變量
     */
    // 屏幕的寬高
    fileprivate let kScreenW = UIScreen.main.bounds.size.width
    fileprivate let kScreenH = UIScreen.main.bounds.size.height
    
    // 代理
    weak var delegate: CarouselViewDelegate?

    // 標(biāo)識當(dāng)前索引值硼砰,默認(rèn)為 0
    fileprivate var currentIndex: Int = 0
    // 開始/結(jié)束拖拽時的x坐標(biāo)值,默認(rèn)為 0
    fileprivate var dragStartX: CGFloat = 0
    fileprivate var dragEndX: CGFloat = 0
    // 記錄cell的總個數(shù)欣硼,默認(rèn)為 0
    fileprivate var dataCount: Int = 0
    // 標(biāo)識是否已經(jīng)計算了 expandCellCount题翰,默認(rèn)為 false
    fileprivate var isCalculateExpandCellCount: Bool = false
    
    // 標(biāo)識是哪個section下的功能,默認(rèn)為第0個
    public var section: Int = 0
    // 是否以page為基礎(chǔ)滑動(即滑動一屏)诈胜,默認(rèn)為 false
    public var isPagingEnabled: Bool = false
    // item距屏幕兩側(cè)的間距豹障,默認(rèn)為 15
    public var sectionMargin: CGFloat = 15 {
        didSet {
            carouselLayout.sectionInset = UIEdgeInsets(top: 0, left: sectionMargin, bottom: 0, right: sectionMargin)
        }
    }
    // item與item之間的間距,默認(rèn)為 10
    public var itemSpacing: CGFloat = 10 {
        didSet {
            carouselLayout.minimumLineSpacing = itemSpacing
            carouselLayout.minimumInteritemSpacing = itemSpacing
        }
    }
    
    // 控件
    public lazy var carouselCollection: UICollectionView = {
        let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.carouselLayout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.backgroundColor = UIColor.white
        return collectionView
    }()
    fileprivate lazy var carouselLayout: UICollectionViewFlowLayout = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = itemSpacing
        layout.minimumInteritemSpacing = itemSpacing
        layout.scrollDirection = .horizontal
        return layout
    }()
    
    // 數(shù)據(jù)源
    public var dataSource: [Any] = [] {
        didSet {
            // 計算cell的總數(shù)量
            self.dataCount = dataSource.count
            calculateTotalCell()
        }
    }
    // 若要循環(huán)滾動效果焦匈,則需更改cell的總數(shù)量
    public var expandCellCount: Int = 0 {
        didSet {
            calculateTotalCell()
        }
    }
    // 從第幾個cell開始顯示的位置
    public var startPosition: Int = 0 {
        didSet {
            if dataSource.count > 0 {
                startPosition = dataSource.count * startPosition
            }
            initCellPosition()
        }
    }
    // item的寬高
    fileprivate var itemWidth: CGFloat {
        get {
            return (kScreenW-sectionMargin*2)
        }
    }
    fileprivate var itemHeight: CGFloat {
        get {
            return self.carouselCollection.frame.size.height - 1
        }
    }
初始化UICollectionView和UICollectionViewFlowLayout血公,在init方法里邊調(diào)用
/**
 *  初始化
 **/
extension CarouselView {
    /*
     *  MARK: - 初始化UI
     */
    fileprivate func setupUI() {
        // 設(shè)置 UICollectionView
        carouselCollection.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        self.addSubview(carouselCollection)
    }
}
實現(xiàn)UICollectionView的協(xié)議方法
/**
 *  UICollectionViewDelegate, UICollectionViewDataSource
 **/
extension CarouselView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataCount
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        // 獲取外部數(shù)據(jù)
        if delegate != nil && ((delegate?.responds(to: #selector(CarouselViewDelegate.collectionView(_:cellForItemAt:)))) ?? false) {
            let cell = delegate?.collectionView(collectionView, cellForItemAt: indexPath)
            if let tempCell = cell {
                return tempCell
            }
        }
        
        return collectionView.dequeueReusableCell(withReuseIdentifier: "other", for: indexPath)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // 返回點(diǎn)擊事件
        if delegate != nil && ((delegate?.responds(to: #selector(CarouselViewDelegate.collectionView(_:didSelectItemAt:)))) ?? false) {
            delegate?.collectionView!(collectionView, didSelectItemAt: indexPath)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        // 獲取外部數(shù)據(jù)
        if delegate != nil && ((delegate?.responds(to: #selector(CarouselViewDelegate.collectionView(_:layout:sizeForItemAt:)))) ?? false) {
            let itemSize = delegate?.collectionView!(collectionView, layout: collectionViewLayout, sizeForItemAt: indexPath)
            if let tempItemSize = itemSize {
                return tempItemSize
            }
        }
        
        return CGSize(width: itemWidth, height: itemHeight)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        // 獲取外部數(shù)據(jù)
        if delegate != nil && ((delegate?.responds(to: #selector(CarouselViewDelegate.collectionView(_:layout:insetForSectionAt:)))) ?? false) {
            let inset = delegate?.collectionView!(collectionView, layout: collectionViewLayout, insetForSectionAt: section)
            if let tempInset = inset {
                return tempInset
            }
        }
        
        return UIEdgeInsets(top: 0, left: sectionMargin, bottom: 0, right: sectionMargin)
    }
}
實現(xiàn)協(xié)議方法,記錄x坐標(biāo)值缓熟,并設(shè)置item的滾動和顯示位置
/**
 *  UIScrollViewDelegate
 **/
extension CarouselView: UIScrollViewDelegate {
    /*
     *  MARK: - 手指拖動開始
     */
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        // 記錄拖拽開始時的x坐標(biāo)的
        self.dragStartX = scrollView.contentOffset.x
    }
    
    /*
     *  MARK: - 手指拖動結(jié)束
     */
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        // 判斷是否按page滑動
        if !isPagingEnabled {
            return
        }
        
        // 記錄拖拽結(jié)束時的x坐標(biāo)的
        self.dragEndX = scrollView.contentOffset.x
        // 主線程刷新UI
        DispatchQueue.main.async {
            self.fixCellToCenter()
        }
    }
}
自定義方法累魔,也是重要的一部分,處理數(shù)據(jù)和cell的方法
/**
 *  計算cell的位置
 **/
extension CarouselView {
    
    /*
     *  MARK: - 計算顯示cell的總數(shù)
     */
    fileprivate func calculateTotalCell() {
        // 判斷是否有數(shù)據(jù)够滑,有則進(jìn)行計算
        if dataSource.count > 0 {
            // 要額外添加的cell數(shù)量大于0垦写,且沒有計算過dataCount屬性值,且dataCount值等于元數(shù)據(jù)的個數(shù)
            if (self.expandCellCount > 0 && !isCalculateExpandCellCount && dataCount <= dataSource.count) {
                // 計算cell的總數(shù)
                self.dataCount = self.dataCount * self.expandCellCount
                
                // 更新標(biāo)識
                self.isCalculateExpandCellCount = true
                
                // 刷新
                self.carouselCollection.reloadData()
                
                initCellPosition()
                return
            }
        }
        
        self.isCalculateExpandCellCount = false
    }
    
    /*
     *  MARK: - 初始化cell的位置
     */
    public func initCellPosition() {
        // 設(shè)置顯示的位置(數(shù)據(jù)大于1條時彰触,初始滾動到中間位置)
        if dataSource.count <= 1 && startPosition <= 0 {
            return
        }
        
        // 若是循環(huán)滑動的話梯投,則初始時先讓cell滑動到某一位置
        if startPosition > 0 && startPosition < dataCount {
            let scrollToIndexPath = IndexPath(item: startPosition, section: section)
            currentIndex = startPosition
            self.carouselCollection.scrollToItem(at: scrollToIndexPath, at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
        }
    }
    
    /*
     *  MARK: - 滑動cell時,計算該顯示的cell
     */
    fileprivate func fixCellToCenter() {
        // 最小滾動距離(用來確定滾動的距離,從而決定是否滾動到下一頁/上一頁)
        let dragMinimumDistance = kScreenW / 2.0 - calculateWidth(60.0)
        
        // 判斷滾動的方向
        if dragStartX - dragEndX >= dragMinimumDistance {
            // 向右
            currentIndex = currentIndex - 1
        } else if dragEndX - dragStartX >= dragMinimumDistance {
            // 向左
            currentIndex = currentIndex + 1
        }
        
        let maximumIndex = carouselCollection.numberOfItems(inSection: section) - 1
        currentIndex = currentIndex <= 0 ? 0 : currentIndex
        currentIndex = currentIndex >= maximumIndex ? maximumIndex : currentIndex
        
        // 滾動到具體的item分蓖,并居中顯示
        let indexPath = IndexPath(item: currentIndex, section: section)
        carouselCollection.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition.centeredHorizontally, animated: true)
    }
}
寬高適配方法
/**
 *  按比例計算寬高
 **/
extension CarouselView {
    /*
     *  MARK: - 計算寬度
     *
     *  @param actualWidth: 實際的寬度
     *  return 返回計算的寬度
     */
    fileprivate func calculateWidth(_ actualWidth: CGFloat) -> CGFloat {
        return (actualWidth * kScreenW / 375.0)
    }
    
    /*
     *  MARK: - 計算高度
     *
     *  @param actualHeight: 實際的高度
     *  return 返回計算的高度
     */
    fileprivate func calculateHeight(_ actualHeight: CGFloat) -> CGFloat {
        return (actualHeight * kScreenH / 667.0)
    }
}

封裝部分到此就結(jié)束了尔艇。



下面我們看一下在ViewController里的使用:

定義變量
  /*
   *  定義變量
   */
  fileprivate lazy var dataSource: [String] = [
        "1.jpg",
        "2.jpg",
        "3.jpg",
        "4.jpg",
        "5.jpg",
    ]
    fileprivate var carouselView: CarouselView?
    // item標(biāo)識符
    public var carouselItemIdentifier: String = "JYCarouselItemIdentifier"
初始化
/**
 *  初始化
 **/
extension ViewController {
    fileprivate func setupUI() {
        carouselView = CarouselView(frame: CGRect(x: 0, y: 100, width: UIScreen.main.bounds.size.width, height: (125 * UIScreen.main.bounds.size.height / 667.0)))
        carouselView?.delegate = self
        carouselView?.startPosition = 100
        carouselView?.dataSource = dataSource
        carouselView?.expandCellCount = 1000
        carouselView?.isPagingEnabled = true
        self.view.addSubview(carouselView!)
        carouselView?.carouselCollection.register(CarouselCollectionCell.classForCoder(), forCellWithReuseIdentifier: carouselItemIdentifier)
    }
}
實現(xiàn)協(xié)議方法,可選的方法可以不實現(xiàn)咆疗,將按默認(rèn)處理
/**
 *  CarouselViewDelegate
 **/
extension ViewController: CarouselViewDelegate {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: carouselItemIdentifier, for: indexPath) as? CarouselCollectionCell
        let currentItem = indexPath.item % dataSource.count // 通過余數(shù)漓帚,取出對應(yīng)的數(shù)據(jù)
        cell?.setData(data: dataSource, currentIndex: currentItem)
        return cell!
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let currentItem = indexPath.item % dataSource.count // 通過余數(shù),取出對應(yīng)的數(shù)據(jù)
        if currentItem < dataSource.count - 1 {
            return CGSize(width: (carouselView?.frame.size.width)!-30, height: (carouselView?.frame.size.height)!-1)
        } else {
            return CGSize(width: 200, height: (carouselView?.frame.size.height)!-1)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print(indexPath.item % dataSource.count)
    }
}

到此就結(jié)束了午磁,如有不妥的地方望指正尝抖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市迅皇,隨后出現(xiàn)的幾起案子昧辽,更是在濱河造成了極大的恐慌,老刑警劉巖登颓,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搅荞,死亡現(xiàn)場離奇詭異,居然都是意外死亡框咙,警方通過查閱死者的電腦和手機(jī)咕痛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喇嘱,“玉大人茉贡,你說我怎么就攤上這事≌咄” “怎么了腔丧?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長作烟。 經(jīng)常有香客問我愉粤,道長,這世上最難降的妖魔是什么拿撩? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任衣厘,我火速辦了婚禮,結(jié)果婚禮上绷雏,老公的妹妹穿的比我還像新娘头滔。我一直安慰自己,他們只是感情好涎显,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布坤检。 她就那樣靜靜地躺著,像睡著了一般期吓。 火紅的嫁衣襯著肌膚如雪早歇。 梳的紋絲不亂的頭發(fā)上倾芝,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機(jī)與錄音箭跳,去河邊找鬼晨另。 笑死,一個胖子當(dāng)著我的面吹牛谱姓,可吹牛的內(nèi)容都是我干的借尿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼屉来,長吁一口氣:“原來是場噩夢啊……” “哼路翻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茄靠,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茂契,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后慨绳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掉冶,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年脐雪,在試婚紗的時候發(fā)現(xiàn)自己被綠了厌小。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡战秋,死狀恐怖召锈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情获询,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布拐袜,位于F島的核電站吉嚣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蹬铺。R本人自食惡果不足惜尝哆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甜攀。 院中可真熱鬧秋泄,春花似錦、人聲如沸规阀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谁撼。三九已至歧胁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喊巍。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工屠缭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人崭参。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓呵曹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親何暮。 傳聞我的和親對象是個殘疾皇子奄喂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355