MultiStreamViewLayout.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. //
  2. // MultiStreamViewLayout.swift
  3. // TUIRoomKit
  4. //
  5. // Created by CY zhao on 2024/10/24.
  6. //
  7. import Foundation
  8. import UIKit
  9. protocol MultiStreamLayoutDelegate: AnyObject {
  10. func updateNumberOfPages(numberOfPages: NSInteger)
  11. }
  12. struct VideoLayoutConfig {
  13. let maxRows: Int
  14. let maxColumns: Int
  15. let spacing: CGFloat
  16. let aspectRatio: CGFloat
  17. }
  18. // MARK: - Layout presets
  19. extension VideoLayoutConfig {
  20. static let `grid` = VideoLayoutConfig(maxRows: 2, maxColumns: 3, spacing: 5.0, aspectRatio: 0)
  21. static let `verticalList` = VideoLayoutConfig(maxRows: 0, maxColumns: 1, spacing: 5.0, aspectRatio: 16/9)
  22. static let `horizontalList` = VideoLayoutConfig(maxRows: 1, maxColumns: 0, spacing: 5.0, aspectRatio: 16/9)
  23. var isPagingEnable: Bool {
  24. return maxRows > 0 && maxColumns > 0
  25. }
  26. var isHorizontalScroll: Bool {
  27. return isPagingEnable || isHorinzontalFlow
  28. }
  29. var isVerticalScroll: Bool {
  30. return isVerticalFlow
  31. }
  32. var isVerticalFlow: Bool {
  33. return maxRows == 0 && maxColumns > 0
  34. }
  35. var isHorinzontalFlow: Bool {
  36. return maxRows > 0 && maxColumns == 0
  37. }
  38. }
  39. class MultiStreamViewLayout: UICollectionViewFlowLayout {
  40. private let config: VideoLayoutConfig
  41. private var contentSize: CGSize = .zero
  42. private var layoutAttributeArray: [UICollectionViewLayoutAttributes] = []
  43. weak var delegate: MultiStreamLayoutDelegate?
  44. init(config: VideoLayoutConfig = .grid) {
  45. self.config = config
  46. super.init()
  47. }
  48. required init?(coder: NSCoder) {
  49. fatalError("init(coder:) has not been implemented")
  50. }
  51. override var collectionViewContentSize: CGSize {
  52. return contentSize
  53. }
  54. private var collectionViewHeight: CGFloat {
  55. return collectionView?.bounds.height ?? UIScreen.main.bounds.height
  56. }
  57. private var collectionViewWidth: CGFloat {
  58. return collectionView?.bounds.width ?? kScreenWidth
  59. }
  60. private var maxShowCellCount: Int {
  61. return config.maxRows * config.maxColumns
  62. }
  63. private var isPaged: Bool {
  64. return config.isPagingEnable
  65. }
  66. private var isVerticalFlow: Bool {
  67. return config.isVerticalFlow
  68. }
  69. private var isHorinzontalFlow: Bool {
  70. return config.isHorinzontalFlow
  71. }
  72. private var isPortrait: Bool {
  73. return collectionViewHeight > collectionViewWidth
  74. }
  75. private var maxColumnsPerPage: Int {
  76. return isPortrait ? min(config.maxRows, config.maxColumns) : max(config.maxRows, config.maxColumns)
  77. }
  78. private var maxRowsPerPage: Int {
  79. return isPortrait ? max(config.maxRows, config.maxColumns) : min(config.maxRows, config.maxColumns)
  80. }
  81. private var itemWidthHeight: CGFloat {
  82. let minimumDistance = min(collectionViewHeight, collectionViewWidth)
  83. let availableSpace = minimumDistance - CGFloat(config.maxColumns + 1) * config.spacing
  84. return availableSpace / CGFloat(config.maxColumns)
  85. }
  86. var isSpeechMode = false
  87. override func prepare() {
  88. super.prepare()
  89. layoutAttributeArray = []
  90. calculateCellFrame()
  91. }
  92. override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  93. return layoutAttributeArray
  94. }
  95. private func calculateCellFrame() {
  96. guard let collectionView = collectionView, isValidConfig() else {
  97. layoutAttributeArray = []
  98. contentSize = .zero
  99. return
  100. }
  101. if isPaged {
  102. calculateConferenceAttributes(itemCount: collectionView.numberOfItems(inSection: 0))
  103. return
  104. }
  105. if isVerticalFlow {
  106. calculateVerticalFlowAttributes(itemCount: collectionView.numberOfItems(inSection: 0))
  107. return
  108. }
  109. if isHorinzontalFlow {
  110. calculateHorizontalFlowAttributes(itemCount: collectionView.numberOfItems(inSection: 0))
  111. return
  112. }
  113. }
  114. private func isValidConfig() -> Bool {
  115. if config.maxRows == 0 && config.maxColumns == 0 {
  116. return false
  117. }
  118. return true
  119. }
  120. }
  121. extension MultiStreamViewLayout {
  122. //MARK: conference layout
  123. private func getFullScreenAttributes(indexPath: IndexPath) ->
  124. UICollectionViewLayoutAttributes {
  125. let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  126. cell.frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: collectionViewHeight)
  127. return cell
  128. }
  129. private var conferenceItemSize: CGFloat {
  130. let minimumDistance = min(collectionViewHeight, collectionViewWidth)
  131. let availableSpace = minimumDistance - CGFloat(maxColumnsPerPage + 1) * config.spacing
  132. if isPortrait {
  133. return availableSpace / CGFloat(maxColumnsPerPage)
  134. } else {
  135. return availableSpace / (CGFloat(maxShowCellCount) / CGFloat(maxColumnsPerPage))
  136. }
  137. }
  138. private func calculateConferenceAttributes(itemCount: Int) {
  139. if isSpeechMode {
  140. calculateSpeechModeAttributes(itemCount: itemCount)
  141. } else {
  142. calculateCommonAttributes(itemCount: itemCount)
  143. }
  144. }
  145. private func calculateSpeechModeAttributes(itemCount: Int) {
  146. guard itemCount > 0 else { return }
  147. let fullScreenCell = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0))
  148. fullScreenCell.frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: collectionViewHeight)
  149. layoutAttributeArray.append(fullScreenCell)
  150. if itemCount == 1 {
  151. contentSize = CGSize(width: collectionViewWidth, height: collectionViewHeight)
  152. return
  153. }
  154. let itemSize = CGSize(width: conferenceItemSize, height: conferenceItemSize)
  155. for i in 1..<itemCount {
  156. let indexPath = IndexPath(item: i, section: 0)
  157. var cell: UICollectionViewLayoutAttributes
  158. if i <= maxShowCellCount - 1 {
  159. cell = getEquallyDividedAttributes(
  160. maxRows: maxRowsPerPage,
  161. maxColumns: maxColumnsPerPage,
  162. indexPath: indexPath,
  163. item: i,
  164. itemCount: itemCount - 1,
  165. cellSize: itemSize,
  166. veriticalCenter: true,
  167. lastRowCenter: false
  168. )
  169. } else {
  170. cell = getEquallyDividedAttributes(
  171. maxRows: maxRowsPerPage,
  172. maxColumns: maxColumnsPerPage,
  173. indexPath: indexPath,
  174. item: i,
  175. itemCount: itemCount - 1,
  176. cellSize: itemSize
  177. )
  178. }
  179. cell.frame = cell.frame.offsetBy(dx: collectionViewWidth, dy: 0)
  180. layoutAttributeArray.append(cell)
  181. }
  182. let pageCount = Int(ceil(CGFloat(itemCount - 1) / CGFloat(maxShowCellCount))) + 1
  183. contentSize = CGSize(width: CGFloat(pageCount) * collectionViewWidth, height: collectionViewHeight)
  184. delegate?.updateNumberOfPages(numberOfPages: pageCount)
  185. }
  186. private func calculateCommonAttributes(itemCount: Int) {
  187. guard itemCount > 0 else { return }
  188. let itemSize = CGSize(width: conferenceItemSize, height: conferenceItemSize)
  189. if itemCount == 1 {
  190. let fullScreenCell = getFullScreenAttributes(indexPath: IndexPath(item: 0, section: 0))
  191. layoutAttributeArray.append(fullScreenCell)
  192. } else {
  193. for i in 0 ... itemCount - 1 {
  194. let indexPath = IndexPath(item: i, section: 0)
  195. var cell: UICollectionViewLayoutAttributes
  196. if i >= maxShowCellCount {
  197. cell = getEquallyDividedAttributes(maxRows: maxRowsPerPage, maxColumns: maxColumnsPerPage, indexPath: indexPath, item: i + 1, itemCount: itemCount, cellSize: itemSize)
  198. } else {
  199. cell = getEquallyDividedAttributes(maxRows: maxRowsPerPage, maxColumns: maxColumnsPerPage, indexPath: indexPath, item: i + 1, itemCount: itemCount, cellSize: itemSize, veriticalCenter: true, lastRowCenter: false)
  200. }
  201. layoutAttributeArray.append(cell)
  202. }
  203. }
  204. let pageCount = Int(ceil(CGFloat(itemCount) / CGFloat(maxShowCellCount)))
  205. contentSize = CGSize(width: CGFloat(pageCount) * collectionViewWidth, height: collectionViewHeight)
  206. delegate?.updateNumberOfPages(numberOfPages: pageCount)
  207. }
  208. //MARK: classroom layout
  209. private func calculatePageDividedAttributes(itemCount: Int) {
  210. let section: Int = 0
  211. let isMultipage = itemCount >= maxShowCellCount
  212. for i in 0 ... itemCount - 1 {
  213. let indexPath = IndexPath(item: i, section: section)
  214. var cell: UICollectionViewLayoutAttributes
  215. if isMultipage {
  216. cell = getEquallyDividedAttributes(maxRows: maxRowsPerPage, maxColumns: maxColumnsPerPage, indexPath: indexPath, item: i + 1, itemCount: itemCount)
  217. } else {
  218. cell = getSinglePagedDividedAttribute(indexPath: indexPath, item: i + 1, itemCount: itemCount)
  219. }
  220. layoutAttributeArray.append(cell)
  221. }
  222. let pageCount = Int(ceil(CGFloat(itemCount) / CGFloat(maxShowCellCount)))
  223. contentSize = CGSize(width: CGFloat(pageCount) * collectionViewWidth, height: collectionViewHeight)
  224. }
  225. private func getSinglePagedDividedAttribute(indexPath: IndexPath, item: Int, itemCount: Int) -> UICollectionViewLayoutAttributes {
  226. let page = Int(ceil(CGFloat(item) / CGFloat(maxShowCellCount)))
  227. let itemsBeforePage = (page - 1) * maxShowCellCount
  228. let currentPageItemCount = min(itemCount, page * maxShowCellCount) - itemsBeforePage
  229. if currentPageItemCount == 1 {
  230. let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  231. cell.frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: collectionViewHeight)
  232. return cell
  233. } else if currentPageItemCount == 2 {
  234. return getEquallyDividedAttributes(maxRows: 1,maxColumns: 2, indexPath: indexPath, item: item, itemCount: itemCount)
  235. } else if currentPageItemCount == 3 {
  236. return getEquallyDividedAttributes(maxRows: 1, maxColumns: 3, indexPath: indexPath, item: item, itemCount: itemCount)
  237. } else if currentPageItemCount == 4 {
  238. return getEquallyDividedAttributes(maxRows: 2, maxColumns: 2, indexPath: indexPath, item: item, itemCount: itemCount)
  239. } else {
  240. return getEquallyDividedAttributes(maxRows: 2, maxColumns: 3, indexPath: indexPath, item: item, itemCount: itemCount, lastRowCenter: true)
  241. }
  242. }
  243. //MARK: size calculator
  244. private func getEquallyDividedAttributes(maxRows: Int,
  245. maxColumns: Int,
  246. indexPath: IndexPath,
  247. item: Int,
  248. itemCount: Int,
  249. cellSize: CGSize? = nil,
  250. veriticalCenter: Bool = false,
  251. lastRowCenter: Bool = false) -> UICollectionViewLayoutAttributes {
  252. let page = Int(ceil(CGFloat(item) / CGFloat(maxShowCellCount)))
  253. let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  254. let currentPageItemCount = min(itemCount - (page - 1) * maxShowCellCount, maxShowCellCount)
  255. let cellWidth: CGFloat
  256. let cellHeight: CGFloat
  257. if let size = cellSize {
  258. cellWidth = size.width
  259. cellHeight = size.height
  260. } else {
  261. cellWidth = (collectionViewWidth - config.spacing * CGFloat(maxColumns - 1)) / CGFloat(maxColumns)
  262. cellHeight = (collectionViewHeight - config.spacing * CGFloat(maxRows - 1)) / CGFloat(maxRows)
  263. }
  264. let contentWidth = cellWidth * CGFloat(maxColumns) + config.spacing * CGFloat(maxColumns - 1)
  265. let totalRows = Int(ceil(CGFloat(currentPageItemCount) / CGFloat(maxColumns)))
  266. let contentHeight = cellHeight * CGFloat(totalRows) + config.spacing * CGFloat(totalRows - 1)
  267. let pageHeight = cellHeight * CGFloat(maxRows) + config.spacing * CGFloat(maxRows - 1)
  268. let startX = (collectionViewWidth - contentWidth) / 2
  269. let startY = veriticalCenter ? (collectionViewHeight - contentHeight) / 2 : (collectionViewHeight - pageHeight) / 2
  270. let beginCellLeft = CGFloat(page - 1) * collectionViewWidth
  271. let itemIndex = item - (page - 1) * maxShowCellCount
  272. let column = (itemIndex - 1) % maxColumns
  273. let row = Int(ceil(CGFloat(itemIndex) / CGFloat(maxColumns))) - 1
  274. let itemY = startY + (cellHeight + config.spacing) * CGFloat(row)
  275. let itemX: CGFloat
  276. let currentRow = Int(ceil(CGFloat(itemIndex) / CGFloat(maxColumns)))
  277. let isLastRow = currentRow == Int(ceil(CGFloat(currentPageItemCount) / CGFloat(maxColumns)))
  278. if isLastRow && lastRowCenter {
  279. let lastRowItemCount = currentPageItemCount - (currentRow - 1) * maxColumns
  280. let lastRowWidth = cellWidth * CGFloat(lastRowItemCount) + config.spacing * CGFloat(lastRowItemCount - 1)
  281. let lastRowBeginX = (collectionViewWidth - lastRowWidth) / 2
  282. itemX = beginCellLeft + lastRowBeginX + (cellWidth + config.spacing) * CGFloat(column)
  283. } else {
  284. itemX = beginCellLeft + startX + (cellWidth + config.spacing) * CGFloat(column)
  285. }
  286. cell.frame = CGRect(x: itemX, y: itemY, width: cellWidth, height: cellHeight)
  287. return cell
  288. }
  289. }
  290. //MARK: one direction flow Layout calculate
  291. extension MultiStreamViewLayout {
  292. private func calculateVerticalFlowAttributes(itemCount: Int) {
  293. guard itemCount > 0 else { return }
  294. let section: Int = 0
  295. let maxColumns = config.maxColumns
  296. let spacing = config.spacing
  297. let totalRows = Int(ceil(CGFloat(itemCount) / CGFloat(maxColumns)))
  298. let cellWidth = (collectionViewWidth - spacing * CGFloat(maxColumns - 1)) / CGFloat(maxColumns)
  299. let cellHeight = cellWidth / config.aspectRatio
  300. let totalHeight = cellHeight * CGFloat(totalRows) + spacing * CGFloat(totalRows - 1)
  301. for i in 0 ... itemCount - 1 {
  302. let indexPath = IndexPath(item: i, section: section)
  303. let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  304. let column = i % maxColumns
  305. let row = i / maxColumns
  306. let itemX = (cellWidth + config.spacing) * CGFloat(column)
  307. var itemY = (cellHeight + config.spacing) * CGFloat(row)
  308. if totalHeight <= collectionViewHeight {
  309. let startY = (collectionViewHeight - totalHeight) / 2
  310. itemY += startY
  311. }
  312. cell.frame = CGRect(x: itemX, y: itemY, width: cellWidth, height: cellHeight)
  313. layoutAttributeArray.append(cell)
  314. }
  315. let contentHeight = cellHeight * CGFloat(totalRows) + spacing * CGFloat(totalRows - 1)
  316. contentSize = CGSize(width: collectionViewWidth, height: contentHeight)
  317. }
  318. private func calculateHorizontalFlowAttributes(itemCount: Int) {
  319. guard itemCount > 0 else { return }
  320. let section: Int = 0
  321. let maxRows = config.maxRows
  322. let spacing = config.spacing
  323. let totalColumns = Int(ceil(CGFloat(itemCount) / CGFloat(maxRows)))
  324. let cellHeight = (collectionViewHeight - config.spacing * CGFloat(maxRows - 1)) / CGFloat(maxRows)
  325. let cellWidth = cellHeight * config.aspectRatio
  326. let totalWidth = cellWidth * CGFloat(totalColumns) + spacing * CGFloat(totalColumns - 1)
  327. for i in 0 ... itemCount - 1 {
  328. let indexPath = IndexPath(item: i, section: section)
  329. let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  330. let row = i % maxRows
  331. let column = i / maxRows
  332. var itemX = (cellWidth + config.spacing) * CGFloat(column)
  333. let itemY = (cellHeight + config.spacing) * CGFloat(row)
  334. if totalWidth <= collectionViewWidth {
  335. let startX = (collectionViewWidth - totalWidth) / 2
  336. itemX += startX
  337. }
  338. cell.frame = CGRect(x: itemX, y: itemY, width: cellWidth, height: cellHeight)
  339. layoutAttributeArray.append(cell)
  340. }
  341. let contentWidth = cellWidth * CGFloat(totalColumns) + config.spacing * CGFloat(totalColumns - 1)
  342. contentSize = CGSize(width: contentWidth, height: collectionViewHeight)
  343. }
  344. }