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

版本記錄

版本號 時(shí)間
V1.0 2019.09.16 星期一

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動力學(xué)和移動效果(一)
2. UIKit框架(二) —— UIKit動力學(xué)和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (二)

開始

首先看下主要內(nèi)容

構(gòu)建一個(gè)受Pinterest應(yīng)用程序啟發(fā)的UICollectionView自定義布局,并學(xué)習(xí)如何緩存屬性并動態(tài)調(diào)整單元格大小。

下面看下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

iOS 6中引入的UICollectionView已成為iOS開發(fā)人員中最受歡迎的UI元素之一。它如此吸引人的是數(shù)據(jù)和表示層之間的分離,這取決于處理布局的單獨(dú)對象。然后,布局負(fù)責(zé)確定視圖的放置和視覺屬性。

您可能使用了默認(rèn)的流布局,即UIKit提供的布局類。這是一個(gè)帶有一些自定義的基本網(wǎng)格布局。

但您也可以實(shí)現(xiàn)自己的自定義布局,以任何方式排列視圖。這使得集合視圖具有靈活性和強(qiáng)大功能。

在這個(gè)UICollectionView自定義布局教程中,您將創(chuàng)建一個(gè)受流行的Pinterest應(yīng)用程序啟發(fā)的布局。

在此過程中,您將學(xué)習(xí):

  • 關(guān)于自定義布局。
  • 如何計(jì)算和緩存布局屬性。
  • 如何處理動態(tài)大小的單元格。

在Xcode中打開下載好的項(xiàng)目并啟動項(xiàng)目。

構(gòu)建并運(yùn)行項(xiàng)目。 你會看到以下內(nèi)容:

該應(yīng)用程序提供了RWDevCon的照片庫。 您可以瀏覽照片,看看與會者在會議期間有多么有趣。

該庫使用具有標(biāo)準(zhǔn)流布局的集合視圖。 乍一看,它看起來還不錯(cuò)。 但你當(dāng)然可以改進(jìn)布局設(shè)計(jì)。

照片并未完全填滿內(nèi)容區(qū)域。 長字幕被截?cái)唷?用戶體驗(yàn)是無聊和靜態(tài)的,因?yàn)樗袉卧翊笮∠嗤?/p>

您可以使用自定義布局改進(jìn)設(shè)計(jì),其中每個(gè)單元格可以自由地滿足其需求。


Creating Custom Collection View Layouts

您將首先為圖庫創(chuàng)建自定義布局類,從而創(chuàng)建令人驚嘆的集合視圖(collection view)

集合視圖布局是抽象類UICollectionViewLayout的子類。 它們定義集合視圖中每個(gè)項(xiàng)目的可視屬性。

各個(gè)屬性是UICollectionViewLayoutAttributes的實(shí)例。 它們包含集合視圖中每個(gè)項(xiàng)目的屬性,例如項(xiàng)目的frametransform

Layouts組中創(chuàng)建一個(gè)新文件。 從iOS ? Source列表中選擇Cocoa Touch Class。 將其命名為PinterestLayout并使其成為UICollectionViewLayout的子類。

接下來,配置集合視圖以使用新布局。 打開Main.storyboard。 在Photo Stream View Controller Scene中選擇Collection View,如下所示:

接下來,打開Attributes inspector。 在Layout下拉列表中選擇Custom。 然后在Class下拉列表中選擇PinterestLayout

好的 - 是時(shí)候看一下它的樣子了。 構(gòu)建并運(yùn)行您的應(yīng)用:

別恐慌! 信不信由你,這是一個(gè)好兆頭。

這意味著集合視圖正在使用您的自定義布局類。 單元格未顯示,因?yàn)?code>PinterestLayout尚未實(shí)現(xiàn)布局過程中涉及的任何方法。


Core Layout Process

想想集合視圖布局過程。 它是集合視圖和布局對象之間的協(xié)作。 當(dāng)集合視圖需要一些布局信息時(shí),它會要求您的布局對象通過按特定順序調(diào)用某些方法來提供它:

您的布局子類必須實(shí)現(xiàn)以下方法:

  • collectionViewContentSize:此方法返回集合視圖內(nèi)容的寬度和高度。您必須實(shí)現(xiàn)它以返回整個(gè)集合視圖內(nèi)容的高度和寬度,而不僅僅是可見內(nèi)容。集合視圖在內(nèi)部使用此信息來配置其滾動視圖的內(nèi)容大小。
  • prepare():每當(dāng)布局操作即將發(fā)生時(shí),UIKit都會調(diào)用此方法。這是您準(zhǔn)備和執(zhí)行確定集合視圖大小和項(xiàng)目位置所需的任何計(jì)算的機(jī)會。
  • layoutAttributesForElements(in :):在此方法中,返回給定矩形內(nèi)所有項(xiàng)的布局屬性。您將屬性作為UICollectionViewLayoutAttributes數(shù)組返回到集合視圖。
  • layoutAttributesForItem(at :):此方法向集合視圖提供按需布局信息。您需要覆蓋它并在請求的indexPath處返回該項(xiàng)的布局屬性。

好的,所以你知道你需要實(shí)現(xiàn)什么。但是你如何計(jì)算這些屬性呢?


Calculating Layout Attributes

對于此布局,您需要?jiǎng)討B(tài)計(jì)算每個(gè)項(xiàng)目的高度,因?yàn)槟孪炔恢勒掌母叨取?您將聲明一個(gè)協(xié)議,當(dāng)PinterestLayout需要它時(shí),它將提供此信息。

現(xiàn)在,回到代碼。 打開PinterestLayout.swift。 在PinterestLayout類之前添加以下委托協(xié)議聲明:

protocol PinterestLayoutDelegate: AnyObject {
  func collectionView(
    _ collectionView: UICollectionView,
    heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
}

此代碼聲明了PinterestLayoutDelegate協(xié)議。 它有一種方法來請求照片的高度。 您很快就會在PhotoStreamViewController中實(shí)現(xiàn)此協(xié)議。

在實(shí)現(xiàn)布局方法之前還有一件事要做。 您需要聲明一些有助于布局過程的屬性。

將以下內(nèi)容添加到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) 這些是用于配置布局的兩個(gè)屬性:列數(shù)和單元格填充。
  • 3) 這是一個(gè)用于緩存計(jì)算屬性的數(shù)組。當(dāng)您調(diào)用prepare()時(shí),您將計(jì)算所有項(xiàng)的屬性并將它們添加到緩存中。當(dāng)集合視圖稍后請求布局屬性時(shí),您可以有效地查詢緩存,而不是每次都重新計(jì)算它們。
  • 4) 這聲明了兩個(gè)屬性來存儲內(nèi)容大小。在添加照片時(shí)增加contentHeight,并根據(jù)集合視圖寬度及其內(nèi)容插入計(jì)算contentWidth。
  • 5) collectionViewContentSize返回集合視圖內(nèi)容的大小。您可以使用前面步驟中的contentWidthcontentHeight來計(jì)算大小。

您已準(zhǔn)備好計(jì)算集合視圖項(xiàng)的屬性?,F(xiàn)在,它將由frame組成。要了解您將如何執(zhí)行此操作,請查看下圖:

您將根據(jù)每個(gè)項(xiàng)目的列以及同一列中上一個(gè)項(xiàng)目的位置來計(jì)算每個(gè)項(xiàng)目的frame。 您可以通過跟蹤framexOffset和上一個(gè)項(xiàng)目的位置yOffset來完成此操作。

您將首先使用項(xiàng)目所屬列的起始X坐標(biāo)來計(jì)算水平位置,然后添加單元格填充。 垂直位置是該列中前一項(xiàng)的起始位置,加上該前一項(xiàng)的高度。 整體項(xiàng)目高度是圖像高度和內(nèi)容填充的總和。

你將在prepare()中做到這一點(diǎn)。 您的主要目標(biāo)是為布局中的每個(gè)項(xiàng)計(jì)算UICollectionViewLayoutAttributes的實(shí)例。

將以下方法添加到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
  }
}

依次記錄每個(gè)編號的的代碼:

  • 1) 如果緩存cache為空且集合視圖存在,則只計(jì)算布局屬性。
  • 2) 根據(jù)列寬度為每列聲明并填充xOffset數(shù)組。 yOffset數(shù)組跟蹤每列的y位置。您將yOffset中的每個(gè)值初始化為0,因?yàn)檫@是每列中第一個(gè)項(xiàng)目的偏移量。
  • 3) 遍歷第一section中的所有項(xiàng)目,因?yàn)榇颂囟ú季种挥幸粋€(gè)部分。
  • 4) 執(zhí)行frame計(jì)算。 width是先前計(jì)算的cellWidth,其中刪除了單元格之間的填充。向代理請求照片的高度,然后根據(jù)此高度和頂部和底部的預(yù)定義cellPadding計(jì)算frame高度。如果沒有設(shè)置代理,請使用默認(rèn)單元格高度。然后,將其與當(dāng)前列的xy偏移量組合,以創(chuàng)建屬性使用的insetFrame
  • 5) 創(chuàng)建UICollectionViewLayoutAttributes的實(shí)例,使用insetFrame設(shè)置其frame并將屬性附加到cache。
  • 6) 展開contentHeight以考慮新計(jì)算項(xiàng)目的frame。然后,根據(jù)frame推進(jìn)當(dāng)前列的yOffset。最后,推進(jìn)column,以便下一個(gè)項(xiàng)目放在下一列中。

注意:由于只要集合視圖的布局變得無效就會調(diào)用prepare(),因此在典型實(shí)現(xiàn)中有許多情況需要在此處重新計(jì)算屬性。例如,當(dāng)方向更改時(shí),UICollectionView的邊界可能會更改。如果從集合中添加或刪除項(xiàng)目,它們也可能會更改。

現(xiàn)在您需要重寫layoutAttributesForElements(in :)。集合視圖在prepare()之后調(diào)用它以確定哪些項(xiàng)在給定矩形中可見。

將以下代碼添加到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添加任何屬性,最終返回到集合視圖。

您必須實(shí)現(xiàn)的最后一個(gè)方法是layoutAttributesForItem(at :)

override func layoutAttributesForItem(at indexPath: IndexPath) 
    -> UICollectionViewLayoutAttributes? {
  return cache[indexPath.item]
}

在這里,您從cache中檢索并返回與請求的indexPath對應(yīng)的布局屬性。


Connecting with UIViewController

在您可以看到正在運(yùn)行的布局之前,您需要實(shí)現(xiàn)布局代理。 PinterestLayout依賴于此來計(jì)算項(xiàng)目frame高度時(shí)的照片和標(biāo)題高度。

打開PhotoStreamViewController.swift。 將以下擴(kuò)展名添加到文件末尾以實(shí)現(xiàn)PinterestLayoutDelegate

extension PhotoStreamViewController: PinterestLayoutDelegate {
  func collectionView(
      _ collectionView: UICollectionView,
      heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
    return photos[indexPath.item].image.size.height
  }
}

在這里,您可以為布局提供照片的精確高度。

接下來,在viewDidLoad()中添加以下代碼,在super調(diào)用的正下方:

if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
  layout.delegate = self
}

這將PhotoStreamViewController設(shè)置為您的布局的代理。

是時(shí)候再看一下了! 構(gòu)建并運(yùn)行您的應(yīng)用程序。 您將看到根據(jù)照片的高度正確定位和調(diào)整單元格:

通過比您想象的更少的工作,您已經(jīng)創(chuàng)建了自己的Pinterest式自定義布局!

如果您想了解有關(guān)自定義布局的更多信息,請考慮以下資源:

后記

本篇主要講述了UICollectionView的自定義布局,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容