UIKit框架(二十六) —— UICollectionView的自定義布局 (一)

版本記錄

版本號 時間
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間的使用示例 (二)

開始

首先看下主要內容

構建一個受Pinterest應用程序啟發(fā)的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的實例驰凛。 它們包含集合視圖中每個項目的屬性胸懈,例如項目的frametransform雅宾。

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返回集合視圖內容的大小多搀。您可以使用前面步驟中的contentWidthcontentHeight來計算大小。

您已準備好計算集合視圖項的屬性≡植浚現(xiàn)在康铭,它將由frame組成。要了解您將如何執(zhí)行此操作赌髓,請查看下圖:

您將根據(jù)每個項目的列以及同一列中上一個項目的位置來計算每個項目的frame从藤。 您可以通過跟蹤framexOffset和上一個項目的位置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高度颖杏。如果沒有設置代理梅垄,請使用默認單元格高度。然后输玷,將其與當前列的xy偏移量組合,以創(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相交的framevisibleLayoutAttributes添加任何屬性赌朋,最終返回到集合視圖。

您必須實現(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式自定義布局坡椒!

如果您想了解有關自定義布局的更多信息扰路,請考慮以下資源:

后記

本篇主要講述了UICollectionView的自定義布局汗唱,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市丈攒,隨后出現(xiàn)的幾起案子哩罪,更是在濱河造成了極大的恐慌,老刑警劉巖巡验,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件际插,死亡現(xiàn)場離奇詭異,居然都是意外死亡显设,警方通過查閱死者的電腦和手機框弛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敷硅,“玉大人功咒,你說我怎么就攤上這事愉阎。” “怎么了力奋?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵榜旦,是天一觀的道長。 經常有香客問我景殷,道長溅呢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任猿挚,我火速辦了婚禮咐旧,結果婚禮上,老公的妹妹穿的比我還像新娘绩蜻。我一直安慰自己铣墨,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布办绝。 她就那樣靜靜地躺著伊约,像睡著了一般。 火紅的嫁衣襯著肌膚如雪孕蝉。 梳的紋絲不亂的頭發(fā)上屡律,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音降淮,去河邊找鬼超埋。 笑死,一個胖子當著我的面吹牛佳鳖,可吹牛的內容都是我干的霍殴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼系吩,長吁一口氣:“原來是場噩夢啊……” “哼繁成!你這毒婦竟也來了?” 一聲冷哼從身側響起淑玫,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎面睛,沒想到半個月后絮蒿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡叁鉴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年土涝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幌墓。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡但壮,死狀恐怖冀泻,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蜡饵,我是刑警寧澤弹渔,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站溯祸,受9級特大地震影響肢专,放射性物質發(fā)生泄漏。R本人自食惡果不足惜焦辅,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一博杖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧筷登,春花似錦剃根、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至镣丑,卻和暖如春舔糖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背莺匠。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工金吗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人趣竣。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓摇庙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親遥缕。 傳聞我的和親對象是個殘疾皇子卫袒,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容