文章按照順序寫的,之前文章寫過的很多邏輯都會略過,建議順序閱讀,并下載源碼結合閱讀。
目錄
項目下載地址: CollectionView-Note
UICollectionView 01 - 基礎布局篇
UICollectionView 02 - 布局和代理篇
UICollectionView 03 - 自定義布局原理篇
UICollectionView 04 - 卡片布局
UICollectionView 05 - 可伸縮Header
UICollectionView 06 - 瀑布流布局
UICollectionView 07 - 標簽布局
上一篇 原理篇 了解一些要實現一個自定義布局需要實現的方法,以及基礎的布局類 UICollectionViewLayout
給我們提供的方法。那么循序漸進本篇來實現一個橫向滾動的卡片布局,這種布局在很多App種也得到使用,有一個不錯的視覺效果。先來看下最終效果。
這種布局其實并不需要我們完全手寫每個元素的位置。只是在原來的位置快要展示的時候做一些縮放。所以我們只需要繼承自 UICollectionViewFlowLayout
流式布局,然后對一些方法進行重寫即可。
首先定義一個繼承自 UICollectionViewFlowLayout
的類 CardLayout
。 并添加一些計算屬性
class CardLayout: UICollectionViewFlowLayout {
/// MARK: - 一些計算屬性 防止編寫冗余代碼
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
}
// 內邊距
private var margin: CGFloat {
return (collectionViewWidth - cellWidth)/2
}
}
然后重寫prepare
進行一些初始化。
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)
}
因為我們并不是完成重寫,還是利用了UICollectionViewFlowLayout
所以需要調用super.prepare()
以及collectionViewContentSize
這些都不是必須重寫的。
上面設定了滾動方向,內邊距,元素大小等
下面是這個卡片布局的重點,layoutAttributesForElements(in: ) -> [UICollectionViewLayoutAttributes]?
, 這個方法需要提供可見區域的UICollectionViewLayoutAttributes
信息,對cell進行布局,我們看到我們這個布局的效果是cell越趨近屏幕的中心 , 就越大 ,遠離則變小。 所以我們只需要拿出原來的attributes 然后根據它距離中心的位置對其進行放射變換
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
原點的位置 - 獲取cell中心距離 屏幕中心位置的絕對值。
- 用上一步獲取的值除以屏幕寬度得到一個縮放比例
- 將cell的縮放范圍規定到 -π/4 到 +π/4之間,并對它執行縮放操作
- 返回處理好的屬性
看起來并不難,這時候運行 ,并沒有達到想要的效果。
為什么呢? 我們在第一個位置滾動到第二個位置的時候第一個位置越來越遠離就會變小,所以每次滾動的時候需要重新計算
shouldInvalidateLayout(forBoundsChange: ) -> Bool
方法登場。
在上個方法后面添加如下方法
// 是否實時刷新布局
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
這時候我們只是寫了一個布局,怎么應用到collectionView上呢。
依舊跟之前一樣,使用Storyboard
(純代碼也很簡單,有對應的初始化方法)。
首先將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
}
}