文章按照順序?qū)懙模拔恼聦戇^的很多邏輯都會略過,建議順序閱讀,并下載源碼結(jié)合閱讀。
目錄
項(xiàng)目下載地址: CollectionView-Note
UICollectionView 01 - 基礎(chǔ)布局篇
UICollectionView 02 - 布局和代理篇
UICollectionView 03 - 自定義布局原理篇
UICollectionView 04 - 卡片布局
UICollectionView 05 - 可伸縮Header
UICollectionView 06 - 瀑布流布局
UICollectionView 07 - 標(biāo)簽布局
上一篇 原理篇 了解一些要實(shí)現(xiàn)一個自定義布局需要實(shí)現(xiàn)的方法,以及基礎(chǔ)的布局類 UICollectionViewLayout 給我們提供的方法。那么循序漸進(jìn)本篇來實(shí)現(xiàn)一個橫向滾動的卡片布局,這種布局在很多App種也得到使用,有一個不錯的視覺效果。先來看下最終效果。

這種布局其實(shí)并不需要我們完全手寫每個元素的位置。只是在原來的位置快要展示的時候做一些縮放。所以我們只需要繼承自 UICollectionViewFlowLayout 流式布局,然后對一些方法進(jìn)行重寫即可。
首先定義一個繼承自 UICollectionViewFlowLayout 的類 CardLayout 。 并添加一些計(jì)算屬性
class CardLayout: UICollectionViewFlowLayout {
/// MARK: - 一些計(jì)算屬性 防止編寫冗余代碼
private var collectionViewHeight: CGFloat {
return collectionView!.frame.height
}
private var collectionViewWidth: CGFloat {
return collectionView!.frame.width
}
private var cellWidth: CGFloat {
return collectionViewWidth*0.7
}
private var cellMargin: CGFloat {
return (collectionViewWidth - cellWidth)/7
}
// 內(nèi)邊距
private var margin: CGFloat {
return (collectionViewWidth - cellWidth)/2
}
}
然后重寫prepare 進(jìn)行一些初始化。
override func prepare() {
super.prepare()
scrollDirection = .horizontal
sectionInset = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
minimumLineSpacing = cellMargin
itemSize = CGSize(width: cellWidth, height: collectionViewHeight*0.75)
}
因?yàn)槲覀儾⒉皇峭瓿芍貙?,還是利用了UICollectionViewFlowLayout 所以需要調(diào)用super.prepare() 以及collectionViewContentSize 這些都不是必須重寫的。
上面設(shè)定了滾動方向,內(nèi)邊距,元素大小等
下面是這個卡片布局的重點(diǎn),layoutAttributesForElements(in: ) -> [UICollectionViewLayoutAttributes]? , 這個方法需要提供可見區(qū)域的UICollectionViewLayoutAttributes 信息,對cell進(jìn)行布局,我們看到我們這個布局的效果是cell越趨近屏幕的中心 , 就越大 ,遠(yuǎn)離則變小。 所以我們只需要拿出原來的attributes 然后根據(jù)它距離中心的位置對其進(jìn)行放射變換
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else { return nil }
// 1
guard let visibleAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
// 2
let centerX = collectionView.contentOffset.x + collectionView.bounds.size.width/2
for attribute in visibleAttributes {
// 3
let distance = abs(attribute.center.x - centerX)
// 4
let aprtScale = distance / collectionView.bounds.size.width
// 5
let scale = abs(cos(aprtScale * CGFloat(Double.pi/4)))
attribute.transform = CGAffineTransform(scaleX: scale, y: scale)
}
// 6
return visibleAttributes
}
這里解釋下:
- 拿到原本的布局信息 (上一篇說過,cell的布局信息存放在
UICollectionViewLayoutAttributes中) - 獲取屏幕中心距離
collectionView原點(diǎn)的位置 - 獲取cell中心距離 屏幕中心位置的絕對值。
- 用上一步獲取的值除以屏幕寬度得到一個縮放比例
- 將cell的縮放范圍規(guī)定到 -π/4 到 +π/4之間,并對它執(zhí)行縮放操作
- 返回處理好的屬性
看起來并不難,這時候運(yùn)行 ,并沒有達(dá)到想要的效果。
為什么呢? 我們在第一個位置滾動到第二個位置的時候第一個位置越來越遠(yuǎn)離就會變小,所以每次滾動的時候需要重新計(jì)算
shouldInvalidateLayout(forBoundsChange: ) -> Bool 方法登場。
在上個方法后面添加如下方法
// 是否實(shí)時刷新布局
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
這時候我們只是寫了一個布局,怎么應(yīng)用到collectionView上呢。
依舊跟之前一樣,使用Storyboard (純代碼也很簡單,有對應(yīng)的初始化方法)。
首先將CollectionView 的layout改為custom , 然后將 layout object改為我們的CardLayout


自定義布局寫好后ViewController中代碼非常簡單了
class CardViewController: UIViewController {
@IBOutlet private weak var collectionView: UICollectionView!
var colors: [UIColor] = []
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
colors = DataManager.shared.generalColors(20)
}
}
// MARK: - UICollectionViewDataSource
extension CardViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BasicsCell.reuseID, for: indexPath) as! BasicsCell
cell.backgroundColor = colors[indexPath.row]
return cell
}
}
