Swift - UICollectionView 使用2

整理一下UICollectionView的使用实现项目中的某些效果。

效果图如下:


Swift - UICollectionView 使用2_第1张图片
效果图.png

实现以上效果,并且可以无限循环,或者不无限循环。
除了以上效果外,还可以自由搭配其他效果,这里就不多说了,可以自己试试。


我们先来了解一下居中显示核心方法,如下:

// 通过indexPath将视图滑动到你指定的item,并且可以设置该item在屏幕中显示的位置(横向:左中右;竖向:上中下)
open func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionViewScrollPosition, animated: Bool)

下面让我们了解一下思路:

  1. 看到这种效果我们应该首先会想到使用UICollectionView,因为它可以自定义布局,横向滑动也比较方便设置等;
  2. 决定使用什么控件实现需求中的效果,我们要开始思考效果中我们都需要对哪些进行设置,也就是UICollectionViewFlowLayout相关属性的设置。从图中我们可以想到要设置itemSize、item之间的间距、item与屏幕之间的间距、横向华动等;
  3. 新建一个CarouselView和CarouselCollectionCell,根据需求初始化控件,这里只是一张本地图片;
  4. 准备的差不多了,就开始编写代码吧,将要用到的东西,定义并初始化实现等等;
  5. 边编写代码变运行看看效果,不至于编写完效果不对还不好找原因;
  6. 大体效果出来后,我们考虑一下如何让每个item都显示在中间,这里就用到了我们上面提到的方法了;
  7. 我们需要让它以page的方式滑动(这里的page和UICollectionView的isPagingEnabled属性是两回事),这里我们需要使用UIScrollViewDelegate中的两个方法,一个是开始拖拽的方法scrollViewWillBeginDragging,另一个是结束拖拽方法scrollViewDidEndDragging,通过这两个方法记录x坐标值,计算出我们要显示的item,并且居中显示。那可能有人会问,为什么不直接设置UICollectionView的isPagingEnabled属性。因为我们的item并不是占屏幕的全部宽度,也就是除了当前的item还要加上两侧的item的一部分才是isPagingEnabled的整个屏幕宽,可想而知滑动后的效果并不是我们想要的那种了,可以自己实验一把;
  8. 封装、优化代码以便外部调用少量代码实现该效果,或其他地方使用。



开始上代码:(这里省略cell的实现,可根据各自需求实现)

以下代码都是在 CarouselView 中
定义协议留待内部调用、外部使用
/**
 *  cell相关协议方法
 **/
@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?

    // 标识当前索引值,默认为 0
    fileprivate var currentIndex: Int = 0
    // 开始/结束拖拽时的x坐标值,默认为 0
    fileprivate var dragStartX: CGFloat = 0
    fileprivate var dragEndX: CGFloat = 0
    // 记录cell的总个数,默认为 0
    fileprivate var dataCount: Int = 0
    // 标识是否已经计算了 expandCellCount,默认为 false
    fileprivate var isCalculateExpandCellCount: Bool = false
    
    // 标识是哪个section下的功能,默认为第0个
    public var section: Int = 0
    // 是否以page为基础滑动(即滑动一屏),默认为 false
    public var isPagingEnabled: Bool = false
    // item距屏幕两侧的间距,默认为 15
    public var sectionMargin: CGFloat = 15 {
        didSet {
            carouselLayout.sectionInset = UIEdgeInsets(top: 0, left: sectionMargin, bottom: 0, right: sectionMargin)
        }
    }
    // item与item之间的间距,默认为 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
    }()
    
    // 数据源
    public var dataSource: [Any] = [] {
        didSet {
            // 计算cell的总数量
            self.dataCount = dataSource.count
            calculateTotalCell()
        }
    }
    // 若要循环滚动效果,则需更改cell的总数量
    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方法里边调用
/**
 *  初始化
 **/
extension CarouselView {
    /*
     *  MARK: - 初始化UI
     */
    fileprivate func setupUI() {
        // 设置 UICollectionView
        carouselCollection.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        self.addSubview(carouselCollection)
    }
}
实现UICollectionView的协议方法
/**
 *  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 {
        
        // 获取外部数据
        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) {
        // 返回点击事件
        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 {

        // 获取外部数据
        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 {
        // 获取外部数据
        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)
    }
}
实现协议方法,记录x坐标值,并设置item的滚动和显示位置
/**
 *  UIScrollViewDelegate
 **/
extension CarouselView: UIScrollViewDelegate {
    /*
     *  MARK: - 手指拖动开始
     */
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        // 记录拖拽开始时的x坐标的
        self.dragStartX = scrollView.contentOffset.x
    }
    
    /*
     *  MARK: - 手指拖动结束
     */
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        // 判断是否按page滑动
        if !isPagingEnabled {
            return
        }
        
        // 记录拖拽结束时的x坐标的
        self.dragEndX = scrollView.contentOffset.x
        // 主线程刷新UI
        DispatchQueue.main.async {
            self.fixCellToCenter()
        }
    }
}
自定义方法,也是重要的一部分,处理数据和cell的方法
/**
 *  计算cell的位置
 **/
extension CarouselView {
    
    /*
     *  MARK: - 计算显示cell的总数
     */
    fileprivate func calculateTotalCell() {
        // 判断是否有数据,有则进行计算
        if dataSource.count > 0 {
            // 要额外添加的cell数量大于0,且没有计算过dataCount属性值,且dataCount值等于元数据的个数
            if (self.expandCellCount > 0 && !isCalculateExpandCellCount && dataCount <= dataSource.count) {
                // 计算cell的总数
                self.dataCount = self.dataCount * self.expandCellCount
                
                // 更新标识
                self.isCalculateExpandCellCount = true
                
                // 刷新
                self.carouselCollection.reloadData()
                
                initCellPosition()
                return
            }
        }
        
        self.isCalculateExpandCellCount = false
    }
    
    /*
     *  MARK: - 初始化cell的位置
     */
    public func initCellPosition() {
        // 设置显示的位置(数据大于1条时,初始滚动到中间位置)
        if dataSource.count <= 1 && startPosition <= 0 {
            return
        }
        
        // 若是循环滑动的话,则初始时先让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)
    }
}

封装部分到此就结束了。



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

定义变量
  /*
   *  定义变量
   */
  fileprivate lazy var dataSource: [String] = [
        "1.jpg",
        "2.jpg",
        "3.jpg",
        "4.jpg",
        "5.jpg",
    ]
    fileprivate var carouselView: CarouselView?
    // item标识符
    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)
    }
}
实现协议方法,可选的方法可以不实现,将按默认处理
/**
 *  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 // 通过余数,取出对应的数据
        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 // 通过余数,取出对应的数据
        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)
    }
}

到此就结束了,如有不妥的地方望指正。

你可能感兴趣的:(Swift - UICollectionView 使用2)