版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.09.16 星期一 |
前言
iOS中有關視圖控件用戶能看到的都在UIKit框架里面纺涤,用戶交互也是通過UIKit進行的犯眠。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動力學和移動效果(一)
2. UIKit框架(二) —— UIKit動力學和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴張效果的實現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴張效果的實現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預加載(二)
11. UIKit框架(十一) —— UICollectionView的重用张漂、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用肢预、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側滑式面板導航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側滑式面板導航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動畫的實現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動畫的實現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉場和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉場和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個APP間的使用示例 (二)
開始
首先看下主要內容
構建一個受
UICollectionView
自定義布局,并學習如何緩存屬性并動態(tài)調整單元格大小微姊。
下面看下寫作環(huán)境
Swift 5, iOS 13, Xcode 11
iOS 6
中引入的UICollectionView
已成為iOS開發(fā)人員中最受歡迎的UI元素之一酸茴。它如此吸引人的是數(shù)據(jù)和表示層之間的分離,這取決于處理布局的單獨對象兢交。然后薪捍,布局負責確定視圖的放置和視覺屬性。
您可能使用了默認的流布局配喳,即UIKit提供的布局類酪穿。這是一個帶有一些自定義的基本網(wǎng)格布局。
但您也可以實現(xiàn)自己的自定義布局晴裹,以任何方式排列視圖被济。這使得集合視圖具有靈活性和強大功能。
在這個UICollectionView
自定義布局教程中涧团,您將創(chuàng)建一個受流行的Pinterest
應用程序啟發(fā)的布局只磷。
在此過程中,您將學習:
- 關于自定義布局少欺。
- 如何計算和緩存布局屬性喳瓣。
- 如何處理動態(tài)大小的單元格。
在Xcode中打開下載好的項目并啟動項目赞别。
構建并運行項目畏陕。 你會看到以下內容:
該應用程序提供了RWDevCon的照片庫。 您可以瀏覽照片仿滔,看看與會者在會議期間有多么有趣惠毁。
該庫使用具有標準流布局的集合視圖。 乍一看崎页,它看起來還不錯鞠绰。 但你當然可以改進布局設計。
照片并未完全填滿內容區(qū)域飒焦。 長字幕被截斷蜈膨。 用戶體驗是無聊和靜態(tài)的屿笼,因為所有單元格大小相同。
您可以使用自定義布局改進設計翁巍,其中每個單元格可以自由地滿足其需求驴一。
Creating Custom Collection View Layouts
您將首先為圖庫創(chuàng)建自定義布局類,從而創(chuàng)建令人驚嘆的集合視圖(collection view)
灶壶。
集合視圖布局是抽象類UICollectionViewLayout
的子類肝断。 它們定義集合視圖中每個項目的可視屬性。
各個屬性是UICollectionViewLayoutAttributes
的實例驰凛。 它們包含集合視圖中每個項目的屬性胸懈,例如項目的frame
或transform
雅宾。
在Layouts
組中創(chuàng)建一個新文件脐供。 從iOS ? Source
列表中選擇Cocoa Touch Class
。 將其命名為PinterestLayout
并使其成為UICollectionViewLayout
的子類星岗。
接下來渔隶,配置集合視圖以使用新布局羔挡。 打開Main.storyboard
。 在Photo Stream View Controller Scene
中選擇Collection View
间唉,如下所示:
接下來,打開Attributes inspector
利术。 在Layout
下拉列表中選擇Custom
呈野。 然后在Class
下拉列表中選擇PinterestLayout
:
好的 - 是時候看一下它的樣子了。 構建并運行您的應用:
別恐慌印叁! 信不信由你被冒,這是一個好兆頭。
這意味著集合視圖正在使用您的自定義布局類轮蜕。 單元格未顯示昨悼,因為PinterestLayout
尚未實現(xiàn)布局過程中涉及的任何方法。
Core Layout Process
想想集合視圖布局過程跃洛。 它是集合視圖和布局對象之間的協(xié)作率触。 當集合視圖需要一些布局信息時,它會要求您的布局對象通過按特定順序調用某些方法來提供它:
您的布局子類必須實現(xiàn)以下方法:
-
collectionViewContentSize
:此方法返回集合視圖內容的寬度和高度汇竭。您必須實現(xiàn)它以返回整個集合視圖內容的高度和寬度葱蝗,而不僅僅是可見內容。集合視圖在內部使用此信息來配置其滾動視圖的內容大小细燎。 -
prepare()
:每當布局操作即將發(fā)生時两曼,UIKit
都會調用此方法。這是您準備和執(zhí)行確定集合視圖大小和項目位置所需的任何計算的機會玻驻。 -
layoutAttributesForElements(in :)
:在此方法中悼凑,返回給定矩形內所有項的布局屬性。您將屬性作為UICollectionViewLayoutAttributes
數(shù)組返回到集合視圖。 -
layoutAttributesForItem(at :)
:此方法向集合視圖提供按需布局信息户辫。您需要覆蓋它并在請求的indexPath
處返回該項的布局屬性渐夸。
好的,所以你知道你需要實現(xiàn)什么寸莫。但是你如何計算這些屬性呢捺萌?
Calculating Layout Attributes
對于此布局,您需要動態(tài)計算每個項目的高度膘茎,因為您事先不知道照片的高度桃纯。 您將聲明一個協(xié)議,當PinterestLayout
需要它時披坏,它將提供此信息态坦。
現(xiàn)在,回到代碼棒拂。 打開PinterestLayout.swift
伞梯。 在PinterestLayout
類之前添加以下委托協(xié)議聲明:
protocol PinterestLayoutDelegate: AnyObject {
func collectionView(
_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
}
此代碼聲明了PinterestLayoutDelegate
協(xié)議。 它有一種方法來請求照片的高度帚屉。 您很快就會在PhotoStreamViewController
中實現(xiàn)此協(xié)議谜诫。
在實現(xiàn)布局方法之前還有一件事要做。 您需要聲明一些有助于布局過程的屬性攻旦。
將以下內容添加到PinterestLayout
:
// 1
weak var delegate: PinterestLayoutDelegate?
// 2
private let numberOfColumns = 2
private let cellPadding: CGFloat = 6
// 3
private var cache: [UICollectionViewLayoutAttributes] = []
// 4
private var contentHeight: CGFloat = 0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
// 5
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
此代碼定義了稍后您需要提供布局信息的一些屬性喻旷。這是一步一步解釋的:
- 1) 這保留了對代理的引用。
- 2) 這些是用于配置布局的兩個屬性:列數(shù)和單元格填充牢屋。
- 3) 這是一個用于緩存計算屬性的數(shù)組且预。當您調用
prepare()
時,您將計算所有項的屬性并將它們添加到緩存中烙无。當集合視圖稍后請求布局屬性時锋谐,您可以有效地查詢緩存,而不是每次都重新計算它們截酷。 - 4) 這聲明了兩個屬性來存儲內容大小涮拗。在添加照片時增加
contentHeight
,并根據(jù)集合視圖寬度及其內容插入計算contentWidth
合搅。 - 5)
collectionViewContentSize
返回集合視圖內容的大小多搀。您可以使用前面步驟中的contentWidth
和contentHeight
來計算大小。
您已準備好計算集合視圖項的屬性≡植浚現(xiàn)在康铭,它將由frame
組成。要了解您將如何執(zhí)行此操作赌髓,請查看下圖:
您將根據(jù)每個項目的列以及同一列中上一個項目的位置來計算每個項目的frame
从藤。 您可以通過跟蹤frame
的xOffset
和上一個項目的位置yOffset
來完成此操作催跪。
您將首先使用項目所屬列的起始X
坐標來計算水平位置,然后添加單元格填充夷野。 垂直位置是該列中前一項的起始位置懊蒸,加上該前一項的高度。 整體項目高度是圖像高度和內容填充的總和悯搔。
你將在prepare()
中做到這一點骑丸。 您的主要目標是為布局中的每個項計算UICollectionViewLayoutAttributes
的實例。
將以下方法添加到PinterestLayout
:
override func prepare() {
// 1
guard
cache.isEmpty,
let collectionView = collectionView
else {
return
}
// 2
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
// 3
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 4
let photoHeight = delegate?.collectionView(
collectionView,
heightForPhotoAtIndexPath: indexPath) ?? 180
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column],
y: yOffset[column],
width: columnWidth,
height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// 5
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
依次記錄每個編號的的代碼:
- 1) 如果緩存
cache
為空且集合視圖存在妒貌,則只計算布局屬性通危。 - 2) 根據(jù)列寬度為每列聲明并填充
xOffset
數(shù)組。yOffset
數(shù)組跟蹤每列的y位置灌曙。您將yOffset
中的每個值初始化為0
菊碟,因為這是每列中第一個項目的偏移量。 - 3) 遍歷第一
section
中的所有項目在刺,因為此特定布局只有一個部分逆害。 - 4) 執(zhí)行
frame
計算。width
是先前計算的cellWidth
蚣驼,其中刪除了單元格之間的填充魄幕。向代理請求照片的高度,然后根據(jù)此高度和頂部和底部的預定義cellPadding
計算frame
高度颖杏。如果沒有設置代理梅垄,請使用默認單元格高度。然后输玷,將其與當前列的x
和y
偏移量組合,以創(chuàng)建屬性使用的insetFrame
靡馁。 - 5) 創(chuàng)建
UICollectionViewLayoutAttributes
的實例欲鹏,使用insetFrame
設置其frame
并將屬性附加到cache
。 - 6) 展開
contentHeight
以考慮新計算項目的frame
臭墨。然后赔嚎,根據(jù)frame
推進當前列的yOffset
。最后胧弛,推進column
尤误,以便下一個項目放在下一列中。
注意:由于只要集合視圖的布局變得無效就會調用
prepare()
结缚,因此在典型實現(xiàn)中有許多情況需要在此處重新計算屬性损晤。例如,當方向更改時红竭,UICollectionView
的邊界可能會更改尤勋。如果從集合中添加或刪除項目喘落,它們也可能會更改。
現(xiàn)在您需要重寫layoutAttributesForElements(in :)
最冰。集合視圖在prepare()
之后調用它以確定哪些項在給定矩形中可見瘦棋。
將以下代碼添加到PinterestLayout
的最后:
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
在這里,您遍歷cache
中的屬性并檢查它們的frame
是否與集合視圖提供的rect
相交暖哨。
使用與該rect
相交的frame
向visibleLayoutAttributes
添加任何屬性赌朋,最終返回到集合視圖。
您必須實現(xiàn)的最后一個方法是layoutAttributesForItem(at :)
篇裁。
override func layoutAttributesForItem(at indexPath: IndexPath)
-> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
在這里沛慢,您從cache
中檢索并返回與請求的indexPath
對應的布局屬性。
Connecting with UIViewController
在您可以看到正在運行的布局之前茴恰,您需要實現(xiàn)布局代理颠焦。 PinterestLayout
依賴于此來計算項目frame
高度時的照片和標題高度。
打開PhotoStreamViewController.swift
往枣。 將以下擴展名添加到文件末尾以實現(xiàn)PinterestLayoutDelegate
:
extension PhotoStreamViewController: PinterestLayoutDelegate {
func collectionView(
_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
return photos[indexPath.item].image.size.height
}
}
在這里伐庭,您可以為布局提供照片的精確高度。
接下來分冈,在viewDidLoad()
中添加以下代碼圾另,在super
調用的正下方:
if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
這將PhotoStreamViewController
設置為您的布局的代理。
是時候再看一下了雕沉! 構建并運行您的應用程序集乔。 您將看到根據(jù)照片的高度正確定位和調整單元格:
通過比您想象的更少的工作,您已經創(chuàng)建了自己的Pinterest
式自定義布局坡椒!
如果您想了解有關自定義布局的更多信息扰路,請考慮以下資源:
- 閱讀Collection View Programming Guide for iOS的Creating Custom Layouts部分,該部分詳細介紹了此主題倔叼。
后記
本篇主要講述了UICollectionView的自定義布局汗唱,感興趣的給個贊或者關注~~~