// Copyright (c) 2019 Open Whisper Systems. All rights reserved. import Foundation import SignalCoreKit protocol GifPickerLayoutDelegate: AnyObject { func imageInfosForLayout() -> [GiphyImageInfo] } // A Pinterest-style waterfall layout. class GifPickerLayout: UICollectionViewLayout { public weak var delegate: GifPickerLayoutDelegate? private var itemAttributesMap = [UInt: 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 = UInt(5) let hInset = UInt(5) let vSpacing = UInt(3) let hSpacing = UInt(3) // We use 2 or 3 columns, depending on the device. // 2 columns will show fewer GIFs at a time, // but use less network & be a more responsive experience. let columnCount = UInt(2) let totalViewWidth = UInt(collectionView.width()) let hTotalWhitespace = (2 * hInset) + (hSpacing * (columnCount - 1)) let hRemainderSpace = totalViewWidth - hTotalWhitespace let columnWidth = UInt(hRemainderSpace / columnCount) // We want to unevenly distribute the hSpacing between the columns // so that the left and right margins are equal, which is non-trivial // due to rounding error. let totalHSpacing = totalViewWidth - ((2 * hInset) + (columnCount * columnWidth)) // columnXs are the left edge of each column. var columnXs = [UInt]() // columnYs are the top edge of the next cell in each column. var columnYs = [UInt]() for columnIndex in 0...columnCount-1 { var columnX = hInset + (columnWidth * columnIndex) if columnCount > 1 { // We want to unevenly distribute the hSpacing between the columns // so that the left and right margins are equal, which is non-trivial // due to rounding error. columnX += ((totalHSpacing * columnIndex) / (columnCount - 1)) } columnXs.append(columnX) columnYs.append(vInset) } // Always layout all items. let imageInfos = delegate.imageInfosForLayout() var contentBottom = vInset for (cellIndex, imageInfo) in imageInfos.enumerated() { // Select a column by finding the "highest, leftmost" column. var column = 0 var cellY = columnYs[column] for (columnValue, columnYValue) in columnYs.enumerated() { if columnYValue < cellY { column = columnValue cellY = columnYValue } } let cellX = columnXs[column] let cellWidth = columnWidth let cellHeight = UInt(columnWidth * imageInfo.originalRendition.height / imageInfo.originalRendition.width) let indexPath = NSIndexPath(row: cellIndex, section: 0) let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath) let itemFrame = CGRect(x: CGFloat(cellX), y: CGFloat(cellY), width: CGFloat(cellWidth), height: CGFloat(cellHeight)) itemAttributes.frame = itemFrame itemAttributesMap[UInt(cellIndex)] = itemAttributes columnYs[column] = cellY + cellHeight + vSpacing contentBottom = max(contentBottom, cellY + cellHeight) } // Add bottom margin. let contentHeight = contentBottom + vInset contentSize = CGSize(width: CGFloat(totalViewWidth), height: CGFloat(contentHeight)) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return itemAttributesMap.values.filter { itemAttributes in return itemAttributes.frame.intersects(rect) } } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let result = itemAttributesMap[UInt(indexPath.row)] return result } override var collectionViewContentSize: CGSize { return contentSize } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { guard let collectionView = collectionView else { return false } return collectionView.width() != newBounds.size.width } }