NCMediaLayout.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. //
  2. // NCMediaLayout.swift
  3. //
  4. // Created by Marino Faggiana on 26/02/24.
  5. // Based on CHTCollectionViewWaterfallLayout by Nelson Tai
  6. // Copyright © 2024 Marino Faggiana. All rights reserved.
  7. //
  8. // This program is free software: you can redistribute it and/or modify
  9. // it under the terms of the GNU General Public License as published by
  10. // the Free Software Foundation, either version 3 of the License, or
  11. // (at your option) any later version.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU General Public License
  19. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. //
  21. import UIKit
  22. public let collectionViewMediaElementKindSectionHeader = "collectionViewMediaElementKindSectionHeader"
  23. public let collectionViewMediaElementKindSectionFooter = "collectionViewMediaElementKindSectionFooter"
  24. protocol NCMediaLayoutDelegate: UICollectionViewDelegate {
  25. func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath, columnCount: Int, mediaLayout: String) -> CGSize
  26. }
  27. public class NCMediaLayout: UICollectionViewLayout {
  28. // MARK: - Private constants
  29. /// How many items to be union into a single rectangle
  30. private let unionSize = 20
  31. // MARK: - Public Properties
  32. public var columnCount: Int = 0 {
  33. didSet {
  34. invalidateIfNotEqual(oldValue, newValue: columnCount)
  35. }
  36. }
  37. public var minimumColumnSpacing: Float = 2.0 {
  38. didSet {
  39. invalidateIfNotEqual(oldValue, newValue: minimumColumnSpacing)
  40. }
  41. }
  42. public var minimumInteritemSpacing: Float = 2.0 {
  43. didSet {
  44. invalidateIfNotEqual(oldValue, newValue: minimumInteritemSpacing)
  45. }
  46. }
  47. public var headerHeight: Float = 0.0 {
  48. didSet {
  49. invalidateIfNotEqual(oldValue, newValue: headerHeight)
  50. }
  51. }
  52. public var footerHeight: Float = 0.0 {
  53. didSet {
  54. invalidateIfNotEqual(oldValue, newValue: footerHeight)
  55. }
  56. }
  57. public var headerInset: UIEdgeInsets = .zero {
  58. didSet {
  59. invalidateIfNotEqual(oldValue, newValue: headerInset)
  60. }
  61. }
  62. public var footerInset: UIEdgeInsets = .zero {
  63. didSet {
  64. invalidateIfNotEqual(oldValue, newValue: footerInset)
  65. }
  66. }
  67. public var sectionInset: UIEdgeInsets = .zero {
  68. didSet {
  69. invalidateIfNotEqual(oldValue, newValue: sectionInset)
  70. }
  71. }
  72. var mediaViewController: NCMedia?
  73. var mediaLayout = ""
  74. public override var collectionViewContentSize: CGSize {
  75. let numberOfSections = collectionView?.numberOfSections
  76. if numberOfSections == 0 {
  77. return CGSize.zero
  78. }
  79. var contentSize = collectionView?.bounds.size
  80. contentSize?.height = CGFloat(columnHeights[0])
  81. return contentSize!
  82. }
  83. // MARK: - Private Properties
  84. private weak var delegate: NCMediaLayoutDelegate? {
  85. return collectionView?.delegate as? NCMediaLayoutDelegate
  86. }
  87. private var columnHeights = [Float]()
  88. private var sectionItemAttributes = [[UICollectionViewLayoutAttributes]]()
  89. private var allItemAttributes = [UICollectionViewLayoutAttributes]()
  90. private var headersAttribute = [Int: UICollectionViewLayoutAttributes]()
  91. private var footersAttribute = [Int: UICollectionViewLayoutAttributes]()
  92. private var unionRects = [CGRect]()
  93. // MARK: - UICollectionViewLayout Methods
  94. public override func prepare() {
  95. super.prepare()
  96. guard let numberOfSections = collectionView?.numberOfSections,
  97. let collectionView = collectionView,
  98. let delegate = delegate else { return }
  99. mediaLayout = NCKeychain().mediaTypeLayout
  100. columnCount = NCKeychain().mediaColumnCount
  101. mediaViewController?.buildMediaPhotoVideo(columnCount: columnCount)
  102. if UIDevice.current.userInterfaceIdiom == .phone,
  103. (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) {
  104. columnCount += 2
  105. }
  106. // Initialize variables
  107. headersAttribute.removeAll(keepingCapacity: false)
  108. footersAttribute.removeAll(keepingCapacity: false)
  109. unionRects.removeAll(keepingCapacity: false)
  110. columnHeights.removeAll(keepingCapacity: false)
  111. allItemAttributes.removeAll(keepingCapacity: false)
  112. sectionItemAttributes.removeAll(keepingCapacity: false)
  113. for _ in 0..<columnCount {
  114. self.columnHeights.append(0)
  115. }
  116. // Create attributes
  117. var top: Float = 0
  118. var attributes: UICollectionViewLayoutAttributes
  119. for section in 0..<numberOfSections {
  120. /*
  121. * 1. Get section-specific metrics (minimumInteritemSpacing, sectionInset)
  122. */
  123. let width = Float(collectionView.frame.size.width - sectionInset.left - sectionInset.right)
  124. let itemWidth = floorf((width - Float(columnCount - 1) * Float(minimumColumnSpacing)) / Float(columnCount))
  125. /*
  126. * 2. Section header
  127. */
  128. top += Float(headerInset.top)
  129. if headerHeight > 0 {
  130. attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: collectionViewMediaElementKindSectionHeader, with: NSIndexPath(item: 0, section: section) as IndexPath)
  131. attributes.frame = CGRect(x: headerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (headerInset.left + headerInset.right), height: CGFloat(headerHeight))
  132. headersAttribute[section] = attributes
  133. allItemAttributes.append(attributes)
  134. top = Float(attributes.frame.maxY) + Float(headerInset.bottom)
  135. }
  136. top += Float(sectionInset.top)
  137. for idx in 0..<columnCount {
  138. columnHeights[idx] = top
  139. }
  140. /*
  141. * 3. Section items
  142. */
  143. let itemCount = collectionView.numberOfItems(inSection: section)
  144. var itemAttributes = [UICollectionViewLayoutAttributes]()
  145. // Item will be put into shortest column.
  146. for idx in 0..<itemCount {
  147. let indexPath = NSIndexPath(item: idx, section: section)
  148. let columnIndex = shortestColumnIndex()
  149. let xOffset = Float(sectionInset.left) + Float(itemWidth + minimumColumnSpacing) * Float(columnIndex)
  150. let yOffset = columnHeights[columnIndex]
  151. let itemSize = delegate.collectionView(collectionView, layout: self, sizeForItemAtIndexPath: indexPath, columnCount: self.columnCount, mediaLayout: self.mediaLayout)
  152. var itemHeight: Float = 0.0
  153. if itemSize.height > 0 && itemSize.width > 0 {
  154. itemHeight = Float(itemSize.height) * itemWidth / Float(itemSize.width)
  155. }
  156. attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
  157. attributes.frame = CGRect(x: CGFloat(xOffset), y: CGFloat(yOffset), width: CGFloat(itemWidth), height: CGFloat(itemHeight))
  158. itemAttributes.append(attributes)
  159. allItemAttributes.append(attributes)
  160. columnHeights[columnIndex] = Float(attributes.frame.maxY) + minimumInteritemSpacing
  161. }
  162. sectionItemAttributes.append(itemAttributes)
  163. /*
  164. * 4. Section footer
  165. */
  166. let columnIndex = longestColumnIndex()
  167. top = columnHeights[columnIndex] - minimumInteritemSpacing + Float(sectionInset.bottom)
  168. top += Float(footerInset.top)
  169. if footerHeight > 0 {
  170. attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: collectionViewMediaElementKindSectionFooter, with: NSIndexPath(item: 0, section: section) as IndexPath)
  171. attributes.frame = CGRect(x: footerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (footerInset.left + footerInset.right), height: CGFloat(footerHeight))
  172. footersAttribute[section] = attributes
  173. allItemAttributes.append(attributes)
  174. top = Float(attributes.frame.maxY) + Float(footerInset.bottom)
  175. }
  176. for idx in 0..<columnCount {
  177. columnHeights[idx] = top
  178. }
  179. }
  180. // Build union rects
  181. var idx = 0
  182. let itemCounts = allItemAttributes.count
  183. while idx < itemCounts {
  184. let rect1 = allItemAttributes[idx].frame
  185. idx = min(idx + unionSize, itemCounts) - 1
  186. let rect2 = allItemAttributes[idx].frame
  187. unionRects.append(rect1.union(rect2))
  188. idx += 1
  189. }
  190. }
  191. public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  192. if indexPath.section >= sectionItemAttributes.count {
  193. return nil
  194. }
  195. if indexPath.item >= sectionItemAttributes[indexPath.section].count {
  196. return nil
  197. }
  198. return sectionItemAttributes[indexPath.section][indexPath.item]
  199. }
  200. public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  201. var attribute: UICollectionViewLayoutAttributes?
  202. if elementKind == collectionViewMediaElementKindSectionHeader {
  203. attribute = headersAttribute[indexPath.section]
  204. } else if elementKind == collectionViewMediaElementKindSectionFooter {
  205. attribute = footersAttribute[indexPath.section]
  206. }
  207. return attribute
  208. }
  209. public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  210. var begin: Int = 0
  211. var end: Int = unionRects.count
  212. var attrs = [UICollectionViewLayoutAttributes]()
  213. for i in 0..<unionRects.count {
  214. if rect.intersects(unionRects[i]) {
  215. begin = i * unionSize
  216. break
  217. }
  218. }
  219. for i in (0..<unionRects.count).reversed() {
  220. if rect.intersects(unionRects[i]) {
  221. end = min((i + 1) * unionSize, allItemAttributes.count)
  222. break
  223. }
  224. }
  225. for i in begin..<end {
  226. let attr = allItemAttributes[i]
  227. if rect.intersects(attr.frame) {
  228. attrs.append(attr)
  229. }
  230. }
  231. return Array(attrs)
  232. }
  233. public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
  234. let oldBounds = collectionView?.bounds
  235. if newBounds.width != oldBounds?.width {
  236. return true
  237. }
  238. return false
  239. }
  240. }
  241. // MARK: - Private Methods
  242. private extension NCMediaLayout {
  243. func shortestColumnIndex() -> Int {
  244. var index: Int = 0
  245. var shortestHeight = MAXFLOAT
  246. for (idx, height) in columnHeights.enumerated() {
  247. if height < shortestHeight {
  248. shortestHeight = height
  249. index = idx
  250. }
  251. }
  252. return index
  253. }
  254. func longestColumnIndex() -> Int {
  255. var index: Int = 0
  256. var longestHeight: Float = 0
  257. for (idx, height) in columnHeights.enumerated() {
  258. if height > longestHeight {
  259. longestHeight = height
  260. index = idx
  261. }
  262. }
  263. return index
  264. }
  265. func invalidateIfNotEqual<T: Equatable>(_ oldValue: T, newValue: T) {
  266. if oldValue != newValue {
  267. invalidateLayout()
  268. }
  269. }
  270. }