通常咱們是使用 UICollectionViewFlowLayout , 熟悉的格子視圖。也能夠自定製 UICollectionViewLayout ,對於每個列表元素,想放哪就放哪。git
這種狀況,系統的就很差直接拿來使了,須要本身定製一個 UICollectionViewLayout.github
通常 new 一個 UICollectionViewLeftAlignedLayout, 繼承自 UICollectionViewFlowLayoutbash
一般要重寫 UICollectionViewFlowLayout 的這兩個方法,app
layoutAttributesForElements(in:)
, 這個方法須要提供,給定的矩形裏面全部的格子的佈局屬性。給定的矩形區域,就是 UICollectioonView 的內容視圖區域 contentSize.異步
layoutAttributesForItem(at:):
這個方法須要提供,格子視圖須要的具體的佈局信息。咱們要重寫這個方法,返回要求的 indexPath 位置上格子的佈局屬性。ide
有時候也要重寫這個屬性:函數
collectionViewContentSize
, 通常咱們是把內容區域的尺寸,做爲計算屬性處理的。他提供格子視圖的內容區域的寬度與高度。格子視圖的內容區域,不是格子視圖的可見區域。佈局
由於格子視圖 UICollectionView,繼承自 UIScrollView。 格子視圖使用該屬性,配置他做爲可滑動視圖 UIScrollView 的內容視圖尺寸。測試
主要代碼見以下:優化
其中輔助函數沒有列出來,具體見文尾的 github repo.
class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
// 這個函數沒有作什麼事情,主要是調用作事情的函數 layoutAttributesForItem,獲取信息,提供出去
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesCopy: [UICollectionViewLayoutAttributes] = []
if let attributes = super.layoutAttributesForElements(in: rect) {
attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
}
for attributes in attributesCopy {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
// 作事情的地方
if let attr = layoutAttributesForItem(at: indexpath) {
attributes.frame = attr.frame
}
}
}
return attributesCopy
}
// 這個函數裏面,具體處理了固定行距列表左排的佈局
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes, let collection = collectionView {
let sectionInset = evaluatedSectionInsetForItem(at: indexPath.section)
let isFirstItemInSection = indexPath.item == 0
let layoutWidth = collection.frame.width - sectionInset.left - sectionInset.right
// 讓每一行的第一個元素排頭,分兩種狀況處理。這是第一種,這個 section 的第一個元素,天然是排頭。
guard !isFirstItemInSection else{
currentItemAttributes.leftAlignFrame(with: sectionInset)
return currentItemAttributes
}
let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero
let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
let currentFrame = currentItemAttributes.frame
// strecthedCurrentFrame 就是把當前格子這一行的區域尺寸,給拉出來了。
// 而後,經過看上一個格子 previous 的位置 frame,是否與前格子這一行的區域尺寸 strecthedCurrentFrame, 有交集。若是有重疊的部分,就在同一行。
let strecthedCurrentFrame = CGRect(x: sectionInset.left,
y: currentFrame.origin.y,
width: layoutWidth,
height: currentFrame.size.height)
let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)
// 讓每一行的第一個元素排頭,分兩種狀況處理。這是第二種,這個 section 的其餘的排頭,算出來,就是:上一個格子在上一行,不在當前行,
guard !isFirstItemInRow else{
currentItemAttributes.leftAlignFrame(with: sectionInset)
return currentItemAttributes
}
// 剩下的,簡單了。統一處理掉。 剩下的格子都不是排頭,與上一個固定間距完了。
var frame = currentItemAttributes.frame
frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacing(at: indexPath.section)
currentItemAttributes.frame = frame
return currentItemAttributes
}
return nil
}
// ...
}
複製代碼
func layoutAttributesForItem(at indexPath: IndexPath)
的設計思路。由於若是使用 UICollectionViewFlowLayout ,什麼都不幹,與上圖的區別就一點。 每一行的元素個數一致,具體也一致,就是那些格子是居中的。畢竟 minimumInteritemSpacing
是最小行內間距的意思,不是固定的行內間距。
而後移一移,就行了。讓每一行的第一個元素排頭,每一行的其餘元素與上一個元素固定間距,這就完了。
OffsetX
,
OffsetX
= CollectionView 的 frame.width - 左邊排列好元素的最後一個 frame.maxX
這是另一種狀況。由於不能改一改左排的 layoutAttributesForItem
方法,就好。左排用的是 previous,沒什麼問題。
右排用 next , 對於每個元素,找 next , 直到其 X + width > collectionView 的 width
, OffsetX
就出來了。
採用 layoutAttributesForItem
找 next, 會有一個比較煩的遞歸,當前找 next, next 找 next. 其實第一種狀況也是這樣,當前找 previous, previous 找 previous.
區別在於蘋果作了優化,next 找 next,由於我在 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
返回的尺寸寬度隨機。
而後就亂套了。當前找 next 返回的隨機的 size, 到了 next, 會返回另外隨機的 size. previous 找 previous, 就沒有這方面的問題。
// 我用了鎖,測試時,每秒刷新一個佈局。次數多了,會出現 Mac 上多核心 CPU 異步繪製的結果不太好
let lock = NSLock()
// 由於要算兩次,第一次就不能放在 func layoutAttributesForElements(in rect: CGRect) 方法,
// 我放在 func prepare() 方法,採用建立的 UICollectionViewLayoutAttributes
override func prepare() {
lock.lock()
contentHeight = 0
cache.removeAll()
storedCellSize.removeAll()
guard let collectionView = collectionView else {
return
}
var currentXOffset:CGFloat = 0
var nextXOffset:CGFloat = 0
var currentYOffset:CGFloat = 0
var nextYOffset:CGFloat = 0
for section in 0..<collectionView.numberOfSections{
let sectionInset = evaluatedSectionInsetForItem(at: section)
nextXOffset = sectionInset.left
nextYOffset += sectionInset.top
let count = collectionView.numberOfItems(inSection: section)
for item in 0..<count{
currentXOffset = nextXOffset
currentYOffset = nextYOffset
let indexPath = IndexPath(item: item, section: section)
// 採用建立的 UICollectionViewLayoutAttributes,不是訪問系統的 super.layoutAttributesForItem(at:)
let currentItemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let currentIndexPathSize = queryItemSize(indexPath)
let currentItemInNext = (currentXOffset + evaluatedMinimumInteritemSpacing(at: section) + currentIndexPathSize.width) > (collectionView.frame.width - sectionInset.right + 0.1)
if currentItemInNext{
currentXOffset = sectionInset.left
currentYOffset += (currentIndexPathSize.height + evaluatedMinimumLineSpacing(at: section))
nextXOffset = currentXOffset + (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))
nextYOffset = currentYOffset
}else{
nextXOffset += (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))
}
let frame = CGRect(origin: CGPoint(x: currentXOffset, y: currentYOffset), size: currentIndexPathSize)
currentItemAttributes.frame = frame
cache[indexPath] = currentItemAttributes
contentHeight = max(contentHeight, frame.maxY)
}
nextYOffset = contentHeight
}
lock.unlock()
}
// 這個函數沒有作什麼事情,主要是調用作事情的函數 layoutAttributesForItem,獲取信息,提供出去
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesCopy: [UICollectionViewLayoutAttributes] = []
if let attributes = super.layoutAttributesForElements(in: rect) {
attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
}
for attributes in attributesCopy {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItem(at: indexpath) {
attributes.frame = attr.frame
}
}
}
return attributesCopy
}
// 這個函數是第二次計算。把第一計算的結果左排,經過算出 OffsetX 添加上去,變成右排
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView,let attribute = cache[indexPath] else {
return nil
}
var offsetX: CGFloat = attribute.frame.maxX
let criteria = collectionView.frame.width - evaluatedSectionInsetForItem(at: indexPath.section).right - 0.1
var gap = criteria - offsetX
var ip = indexPath
let sectionCount = collectionView.numberOfItems(inSection: indexPath.section)
while ip.item < sectionCount{
// 經過 Y 值來比較,結果比較穩定。同一行嘛,Y 座標,天然是一致的。
var conditionSecond = false
if let nextAttri = cache[ip.next]{
conditionSecond = nextAttri.frame.minY != attribute.frame.minY
}
if (ip.item + 1) >= sectionCount || conditionSecond {
gap = criteria - offsetX
break
}
else{
ip = ip.next
offsetX += (evaluatedMinimumInteritemSpacing(at: indexPath.section) + cache[ip]!.frame.width)
}
}
attribute.trailingAlignFrame(with: gap)
return attribute
}
複製代碼
這裏用到了 prepare()
方法,當有佈局操做的時候,就調用這個方法。 咱們用這個時機,計算出提供給 collectionView 的尺寸和這些格子的位置。
同例子一,把每行第一個元素,鋪到最右端,其他的格子保持固定間距就行了。
代碼以下:
// 這個函數沒有作什麼事情,主要是調用作事情的函數 layoutAttributesForItem,獲取信息,提供出去
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesCopy: [UICollectionViewLayoutAttributes] = []
if let attributes = super.layoutAttributesForElements(in: rect) {
attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
}
for attributes in attributesCopy {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItem(at: indexpath) {
attributes.frame = attr.frame
}
}
}
return attributesCopy
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes , let collection = collectionView{
let isFirstItemInSection = indexPath.item == 0
// 右邊派頭狀況一
if isFirstItemInSection {
currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)
return currentItemAttributes
}
let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero
let currentFrame = currentItemAttributes.frame
let strecthedCurrentFrame = CGRect(x: 0,
y: currentFrame.origin.y,
width: collection.frame.size.width,
height: currentFrame.size.height)
let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)
// 右邊派頭狀況二
if isFirstItemInRow {
currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)
return currentItemAttributes
}
// 右邊正常的狀況
let previousFrameLeftPoint = previousFrame.origin.x
var frame = currentItemAttributes.frame
let minimumInteritemSpacing = evaluatedMinimumInteritemSpacing(at: indexPath.item)
frame.origin.x = previousFrameLeftPoint - minimumInteritemSpacing - frame.size.width
currentItemAttributes.frame = frame
return currentItemAttributes
}
return nil
}
複製代碼
更多: