// // Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation import SignalRingRTC protocol GroupCallVideoGridLayoutDelegate: AnyObject { var maxColumns: Int { get } var maxRows: Int { get } var maxItems: Int { get } func deviceState(for index: Int) -> RemoteDeviceState? } class GroupCallVideoGridLayout: UICollectionViewLayout { public weak var delegate: GroupCallVideoGridLayoutDelegate? private var itemAttributesMap = [Int: UICollectionViewLayoutAttributes]() private var contentSize = CGSize.zero // MARK: Initializers and Factory Methods @available(*, unavailable, message: "use other constructor instead.") required init?(coder aDecoder: NSCoder) { notImplemented() } override init() { super.init() } // MARK: Methods override func invalidateLayout() { super.invalidateLayout() itemAttributesMap.removeAll() } override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { super.invalidateLayout(with: context) itemAttributesMap.removeAll() } override func prepare() { super.prepare() guard let collectionView = collectionView else { return } guard let delegate = delegate else { return } let vInset: CGFloat = 6 let hInset: CGFloat = 6 let vSpacing: CGFloat = 6 let hSpacing: CGFloat = 6 let maxColumns = delegate.maxColumns let maxRows = delegate.maxRows let numberOfItems = min(collectionView.numberOfItems(inSection: 0), delegate.maxItems) guard numberOfItems > 0 else { return } // We evenly distribute items across rows, up to the max // column count. If an item is alone on a row, it should // expand across all columns. let possibleGrids = (1...maxColumns).reduce( into: [(rows: Int, columns: Int)]() ) { result, columns in let rows = Int(ceil(CGFloat(numberOfItems) / CGFloat(columns))) if let previousRows = result.last?.rows, previousRows == rows { return } result.append((rows, columns)) }.filter { $0.columns <= maxColumns && $0.rows <= maxRows } .sorted { lhs, rhs in // We prefer to render square grids (e.g. 2x2, 3x3, etc.) but it's // not always possible depending on how many items we have available. // If a square aspect ratio is not possible, we'll defer to having more // rows than columns. let lhsDistanceFromSquare = CGFloat(lhs.rows) / CGFloat(lhs.columns) - 1 let rhsDistanceFromSquare = CGFloat(rhs.rows) / CGFloat(rhs.columns) - 1 if lhsDistanceFromSquare >= 0 && rhsDistanceFromSquare >= 0 { return lhsDistanceFromSquare < rhsDistanceFromSquare } else { return lhsDistanceFromSquare > rhsDistanceFromSquare } } guard let (numberOfRows, numberOfColumns) = possibleGrids.first else { return owsFailDebug("missing grid") } let totalViewWidth = collectionView.width let totalViewHeight = collectionView.height let verticalSpacersWidth = (2 * vInset) + (vSpacing * (CGFloat(numberOfRows) - 1)) let verticalCellSpace = totalViewHeight - verticalSpacersWidth let rowHeight = verticalCellSpace / CGFloat(numberOfRows) // The last row may have less columns than the previous rows, // if there is an odd number of videos. Each row should always // expand the full width of the collection view. var columnWidthPerRow = [CGFloat]() for row in 1...numberOfRows { let numberOfColumnsForRow: Int if row == numberOfRows { numberOfColumnsForRow = numberOfItems - ((row - 1) * numberOfColumns) } else { numberOfColumnsForRow = numberOfColumns } let horizontalSpacersWidth = (2 * hInset) + (hSpacing * (CGFloat(numberOfColumnsForRow) - 1)) let horizontalCellSpace = totalViewWidth - horizontalSpacersWidth let columnWidth = horizontalCellSpace / CGFloat(numberOfColumnsForRow) columnWidthPerRow.append(columnWidth) } for index in 0.. [UICollectionViewLayoutAttributes]? { return itemAttributesMap.values.filter { itemAttributes in return itemAttributes.frame.intersects(rect) } } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return itemAttributesMap[indexPath.row] } override var collectionViewContentSize: CGSize { return contentSize } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } }