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