|
| 1 | +import UIKit |
| 2 | + |
| 3 | +public let UICollectionElementKindSectionBackground = "UICollectionElementKindSectionBackground" |
| 4 | + |
| 5 | +open class UICollectionViewFlexLayout: UICollectionViewLayout { |
| 6 | + private(set) var layoutAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:] |
| 7 | + private(set) var backgroundAttributes: [Int: UICollectionViewLayoutAttributes] = [:] |
| 8 | + private(set) var cachedContentSize: CGSize = .zero |
| 9 | + |
| 10 | + override open func prepare() { |
| 11 | + guard let collectionView = self.collectionView else { return } |
| 12 | + |
| 13 | + let contentWidth = collectionView.frame.width |
| 14 | + var offset: CGPoint = .zero |
| 15 | + |
| 16 | + self.layoutAttributes.removeAll() |
| 17 | + for section in 0..<collectionView.numberOfSections { |
| 18 | + let sectionVerticalSpacing: CGFloat |
| 19 | + if section > 0 { |
| 20 | + sectionVerticalSpacing = self.verticalSpacing(betweenSectionAt: section - 1, and: section) |
| 21 | + } else { |
| 22 | + sectionVerticalSpacing = 0 |
| 23 | + } |
| 24 | + let sectionMargin = self.margin(forSectionAt: section) |
| 25 | + let sectionPadding = self.padding(forSectionAt: section) |
| 26 | + |
| 27 | + // maximum value of (height + padding bottom + margin bottom) in current row |
| 28 | + var maxItemBottom: CGFloat = 0 |
| 29 | + |
| 30 | + offset.x = sectionMargin.left + sectionPadding.left // start from left |
| 31 | + offset.y += sectionVerticalSpacing + sectionMargin.top + sectionPadding.top // accumulated |
| 32 | + |
| 33 | + for item in 0..<collectionView.numberOfItems(inSection: section) { |
| 34 | + let indexPath = IndexPath(item: item, section: section) |
| 35 | + let itemMargin = self.margin(forItemAt: indexPath) |
| 36 | + let itemPadding = self.padding(forItemAt: indexPath) |
| 37 | + let itemSize = self.size(forItemAt: indexPath) |
| 38 | + |
| 39 | + if item > 0 { |
| 40 | + offset.x += self.horizontalSpacing(betweenItemAt: IndexPath(item: item - 1, section: section), and: indexPath) |
| 41 | + } |
| 42 | + if offset.x + itemMargin.left + itemPadding.left + itemSize.width + itemPadding.right + itemMargin.right + sectionPadding.right + sectionMargin.right > contentWidth { |
| 43 | + offset.x = sectionMargin.left + sectionPadding.left // start from left |
| 44 | + offset.y += maxItemBottom // next line |
| 45 | + if item > 0 { |
| 46 | + offset.y += self.verticalSpacing(betweenItemAt: IndexPath(item: item - 1, section: section), and: indexPath) |
| 47 | + } |
| 48 | + maxItemBottom = 0 |
| 49 | + } |
| 50 | + |
| 51 | + let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) |
| 52 | + attributes.size = itemSize |
| 53 | + attributes.frame.origin.x = offset.x + itemMargin.left + itemPadding.left |
| 54 | + attributes.frame.origin.y = offset.y + itemMargin.top + itemPadding.top |
| 55 | + |
| 56 | + offset.x += itemSize.width + itemPadding.right + itemMargin.right |
| 57 | + maxItemBottom = max(maxItemBottom, itemMargin.top + itemPadding.top + itemSize.height + itemPadding.bottom + itemMargin.bottom) |
| 58 | + self.layoutAttributes[indexPath] = attributes |
| 59 | + } |
| 60 | + |
| 61 | + offset.y += maxItemBottom + sectionPadding.bottom + sectionMargin.bottom |
| 62 | + self.cachedContentSize = CGSize(width: contentWidth, height: offset.y) |
| 63 | + } |
| 64 | + |
| 65 | + self.backgroundAttributes.removeAll() |
| 66 | + for section in 0..<collectionView.numberOfSections { |
| 67 | + let layoutAttributes = self.layoutAttributes.lazy.filter { $0.key.section == section }.map { $0.value } |
| 68 | + guard let minXAttribute = layoutAttributes.min(by: { $0.frame.minX < $1.frame.minX }) else { continue } |
| 69 | + guard let minYAttribute = layoutAttributes.min(by: { $0.frame.minY < $1.frame.minY }) else { continue } |
| 70 | + guard let maxXAttribute = layoutAttributes.max(by: { $0.frame.maxX < $1.frame.maxX }) else { continue } |
| 71 | + guard let maxYAttribute = layoutAttributes.max(by: { $0.frame.maxY < $1.frame.maxY }) else { continue } |
| 72 | + let (minX, minY) = (minXAttribute.frame.minX, minYAttribute.frame.minY) |
| 73 | + let (maxX, maxY) = (maxXAttribute.frame.maxX, maxYAttribute.frame.maxY) |
| 74 | + let (width, height) = (maxX - minX, maxY - minY) |
| 75 | + guard width > 0 && height > 0 else { continue } |
| 76 | + |
| 77 | + let sectionPadding = self.padding(forSectionAt: section) |
| 78 | + let attributes = UICollectionViewLayoutAttributes( |
| 79 | + forSupplementaryViewOfKind: UICollectionElementKindSectionBackground, |
| 80 | + with: IndexPath(item: 0, section: section) |
| 81 | + ) |
| 82 | + let itemPaddingLeft = self.padding(forItemAt: minXAttribute.indexPath).left |
| 83 | + let itemPaddingTop = self.padding(forItemAt: minYAttribute.indexPath).top |
| 84 | + let itemPaddingRight = self.padding(forItemAt: maxXAttribute.indexPath).right |
| 85 | + let itemPaddingBottom = self.padding(forItemAt: maxYAttribute.indexPath).bottom |
| 86 | + attributes.frame = CGRect( |
| 87 | + x: minX - sectionPadding.left - itemPaddingLeft, |
| 88 | + y: minY - sectionPadding.top - itemPaddingTop, |
| 89 | + width: width + sectionPadding.left + sectionPadding.right + itemPaddingLeft + itemPaddingRight, |
| 90 | + height: height + sectionPadding.top + sectionPadding.bottom + itemPaddingTop + itemPaddingBottom |
| 91 | + ) |
| 92 | + attributes.zIndex = -1 |
| 93 | + self.backgroundAttributes[section] = attributes |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + override open var collectionViewContentSize: CGSize { |
| 98 | + return self.cachedContentSize |
| 99 | + } |
| 100 | + |
| 101 | + override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { |
| 102 | + return self.layoutAttributes.values.filter { $0.frame.intersects(rect) } |
| 103 | + + self.backgroundAttributes.values.filter { $0.frame.intersects(rect) } |
| 104 | + } |
| 105 | + |
| 106 | + override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { |
| 107 | + return self.layoutAttributes[indexPath] |
| 108 | + } |
| 109 | + |
| 110 | + override open func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { |
| 111 | + guard elementKind == UICollectionElementKindSectionBackground else { return nil } |
| 112 | + guard indexPath.item == 0 else { return nil } |
| 113 | + return self.backgroundAttributes[indexPath.section] |
| 114 | + } |
| 115 | + |
| 116 | + open func maximumWidth(forItemAt indexPath: IndexPath) -> CGFloat { |
| 117 | + guard let collectionView = self.collectionView else { return 0 } |
| 118 | + let sectionMargin = self.margin(forSectionAt: indexPath.section) |
| 119 | + let sectionPadding = self.padding(forSectionAt: indexPath.section) |
| 120 | + let itemMargin = self.margin(forItemAt: indexPath) |
| 121 | + let itemPadding = self.padding(forItemAt: indexPath) |
| 122 | + return collectionView.frame.width |
| 123 | + - sectionMargin.left |
| 124 | + - sectionPadding.left |
| 125 | + - itemMargin.left |
| 126 | + - itemPadding.left |
| 127 | + - itemPadding.right |
| 128 | + - itemMargin.right |
| 129 | + - sectionPadding.right |
| 130 | + - sectionMargin.right |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +extension UICollectionViewFlexLayout { |
| 135 | + var delegate: UICollectionViewDelegateFlexLayout? { |
| 136 | + return self.collectionView?.delegate as? UICollectionViewDelegateFlexLayout |
| 137 | + } |
| 138 | + |
| 139 | + func size(forItemAt indexPath: IndexPath) -> CGSize { |
| 140 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return .zero } |
| 141 | + return delegate.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) ?? .zero |
| 142 | + } |
| 143 | + |
| 144 | + func verticalSpacing(betweenSectionAt section: Int, and nextSection: Int) -> CGFloat { |
| 145 | + guard section != nextSection else { return 0 } |
| 146 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return 0 } |
| 147 | + return delegate.collectionView?(collectionView, layout: self, verticalSpacingBetweenSectionAt: section, and: nextSection) ?? 0 |
| 148 | + } |
| 149 | + |
| 150 | + func margin(forSectionAt section: Int) -> UIEdgeInsets { |
| 151 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return .zero } |
| 152 | + return delegate.collectionView?(collectionView, layout: self, marginForSectionAt: section) ?? .zero |
| 153 | + } |
| 154 | + |
| 155 | + func padding(forSectionAt section: Int) -> UIEdgeInsets { |
| 156 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return .zero } |
| 157 | + return delegate.collectionView?(collectionView, layout: self, paddingForSectionAt: section) ?? .zero |
| 158 | + } |
| 159 | + |
| 160 | + func horizontalSpacing(betweenItemAt indexPath: IndexPath, and nextIndexPath: IndexPath) -> CGFloat { |
| 161 | + guard indexPath != nextIndexPath else { return 0 } |
| 162 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return 0 } |
| 163 | + return delegate.collectionView?(collectionView, layout: self, horizontalSpacingBetweenItemAt: indexPath, and: nextIndexPath) ?? 0 |
| 164 | + } |
| 165 | + |
| 166 | + func verticalSpacing(betweenItemAt indexPath: IndexPath, and nextIndexPath: IndexPath) -> CGFloat { |
| 167 | + guard indexPath != nextIndexPath else { return 0 } |
| 168 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return 0 } |
| 169 | + return delegate.collectionView?(collectionView, layout: self, verticalSpacingBetweenItemAt: indexPath, and: nextIndexPath) ?? 0 |
| 170 | + } |
| 171 | + |
| 172 | + func margin(forItemAt indexPath: IndexPath) -> UIEdgeInsets { |
| 173 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return .zero } |
| 174 | + return delegate.collectionView?(collectionView, layout: self, marginForItemAt: indexPath) ?? .zero |
| 175 | + } |
| 176 | + |
| 177 | + func padding(forItemAt indexPath: IndexPath) -> UIEdgeInsets { |
| 178 | + guard let collectionView = self.collectionView, let delegate = self.delegate else { return .zero } |
| 179 | + return delegate.collectionView?(collectionView, layout: self, paddingForItemAt: indexPath) ?? .zero |
| 180 | + } |
| 181 | +} |
0 commit comments