整理一下UICollectionView的使用实现项目中的某些效果。
效果图如下:
实现以上效果,并且可以无限循环,或者不无限循环。
除了以上效果外,还可以自由搭配其他效果,这里就不多说了,可以自己试试。
我们先来了解一下居中显示核心方法,如下:
// 通过indexPath将视图滑动到你指定的item,并且可以设置该item在屏幕中显示的位置(横向:左中右;竖向:上中下)
open func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionViewScrollPosition, animated: Bool)
下面让我们了解一下思路:
- 看到这种效果我们应该首先会想到使用UICollectionView,因为它可以自定义布局,横向滑动也比较方便设置等;
- 决定使用什么控件实现需求中的效果,我们要开始思考效果中我们都需要对哪些进行设置,也就是UICollectionViewFlowLayout相关属性的设置。从图中我们可以想到要设置itemSize、item之间的间距、item与屏幕之间的间距、横向华动等;
- 新建一个CarouselView和CarouselCollectionCell,根据需求初始化控件,这里只是一张本地图片;
- 准备的差不多了,就开始编写代码吧,将要用到的东西,定义并初始化实现等等;
- 边编写代码变运行看看效果,不至于编写完效果不对还不好找原因;
- 大体效果出来后,我们考虑一下如何让每个item都显示在中间,这里就用到了我们上面提到的方法了;
- 我们需要让它以page的方式滑动(这里的page和UICollectionView的isPagingEnabled属性是两回事),这里我们需要使用UIScrollViewDelegate中的两个方法,一个是开始拖拽的方法scrollViewWillBeginDragging,另一个是结束拖拽方法scrollViewDidEndDragging,通过这两个方法记录x坐标值,计算出我们要显示的item,并且居中显示。那可能有人会问,为什么不直接设置UICollectionView的isPagingEnabled属性。因为我们的item并不是占屏幕的全部宽度,也就是除了当前的item还要加上两侧的item的一部分才是isPagingEnabled的整个屏幕宽,可想而知滑动后的效果并不是我们想要的那种了,可以自己实验一把;
- 封装、优化代码以便外部调用少量代码实现该效果,或其他地方使用。
开始上代码:(这里省略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)
}
}
到此就结束了,如有不妥的地方望指正。