123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- //
- // UICollectionViewWaterfallLayout.m
- //
- // Created by Nelson on 12/11/19.
- // Copyright (c) 2012 Nelson Tai. All rights reserved.
- //
- #import "CHTCollectionViewWaterfallLayout.h"
- #import "tgmath.h"
- NSString *const CHTCollectionElementKindSectionHeader = @"CHTCollectionElementKindSectionHeader";
- NSString *const CHTCollectionElementKindSectionFooter = @"CHTCollectionElementKindSectionFooter";
- @interface CHTCollectionViewWaterfallLayout ()
- /// The delegate will point to collection view's delegate automatically.
- @property (nonatomic, weak) id <CHTCollectionViewDelegateWaterfallLayout> delegate;
- /// Array to store height for each column
- @property (nonatomic, strong) NSMutableArray *columnHeights;
- /// Array of arrays. Each array stores item attributes for each section
- @property (nonatomic, strong) NSMutableArray *sectionItemAttributes;
- /// Array to store attributes for all items includes headers, cells, and footers
- @property (nonatomic, strong) NSMutableArray *allItemAttributes;
- /// Dictionary to store section headers' attribute
- @property (nonatomic, strong) NSMutableDictionary *headersAttribute;
- /// Dictionary to store section footers' attribute
- @property (nonatomic, strong) NSMutableDictionary *footersAttribute;
- /// Array to store union rectangles
- @property (nonatomic, strong) NSMutableArray *unionRects;
- @end
- @implementation CHTCollectionViewWaterfallLayout
- /// How many items to be union into a single rectangle
- static const NSInteger unionSize = 20;
- static CGFloat CHTFloorCGFloat(CGFloat value) {
- CGFloat scale = [UIScreen mainScreen].scale;
- return floor(value * scale) / scale;
- }
- #pragma mark - Public Accessors
- - (void)setColumnCount:(NSInteger)columnCount {
- if (_columnCount != columnCount) {
- _columnCount = columnCount;
- [self invalidateLayout];
- }
- }
- - (void)setMinimumColumnSpacing:(CGFloat)minimumColumnSpacing {
- if (_minimumColumnSpacing != minimumColumnSpacing) {
- _minimumColumnSpacing = minimumColumnSpacing;
- [self invalidateLayout];
- }
- }
- - (void)setMinimumInteritemSpacing:(CGFloat)minimumInteritemSpacing {
- if (_minimumInteritemSpacing != minimumInteritemSpacing) {
- _minimumInteritemSpacing = minimumInteritemSpacing;
- [self invalidateLayout];
- }
- }
- - (void)setHeaderHeight:(CGFloat)headerHeight {
- if (_headerHeight != headerHeight) {
- _headerHeight = headerHeight;
- [self invalidateLayout];
- }
- }
- - (void)setFooterHeight:(CGFloat)footerHeight {
- if (_footerHeight != footerHeight) {
- _footerHeight = footerHeight;
- [self invalidateLayout];
- }
- }
- - (void)setHeaderInset:(UIEdgeInsets)headerInset {
- if (!UIEdgeInsetsEqualToEdgeInsets(_headerInset, headerInset)) {
- _headerInset = headerInset;
- [self invalidateLayout];
- }
- }
- - (void)setFooterInset:(UIEdgeInsets)footerInset {
- if (!UIEdgeInsetsEqualToEdgeInsets(_footerInset, footerInset)) {
- _footerInset = footerInset;
- [self invalidateLayout];
- }
- }
- - (void)setSectionInset:(UIEdgeInsets)sectionInset {
- if (!UIEdgeInsetsEqualToEdgeInsets(_sectionInset, sectionInset)) {
- _sectionInset = sectionInset;
- [self invalidateLayout];
- }
- }
- - (void)setItemRenderDirection:(CHTCollectionViewWaterfallLayoutItemRenderDirection)itemRenderDirection {
- if (_itemRenderDirection != itemRenderDirection) {
- _itemRenderDirection = itemRenderDirection;
- [self invalidateLayout];
- }
- }
- - (NSInteger)columnCountForSection:(NSInteger)section {
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:columnCountForSection:)]) {
- return [self.delegate collectionView:self.collectionView layout:self columnCountForSection:section];
- } else {
- return self.columnCount;
- }
- }
- - (CGFloat)itemWidthInSectionAtIndex:(NSInteger)section {
- UIEdgeInsets sectionInset;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
- sectionInset = [self.delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:section];
- } else {
- sectionInset = self.sectionInset;
- }
- CGFloat width = self.collectionView.bounds.size.width - sectionInset.left - sectionInset.right;
- NSInteger columnCount = [self columnCountForSection:section];
- CGFloat columnSpacing = self.minimumColumnSpacing;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:minimumColumnSpacingForSectionAtIndex:)]) {
- columnSpacing = [self.delegate collectionView:self.collectionView layout:self minimumColumnSpacingForSectionAtIndex:section];
- }
- return CHTFloorCGFloat((width - (columnCount - 1) * columnSpacing) / columnCount);
- }
- #pragma mark - Private Accessors
- - (NSMutableDictionary *)headersAttribute {
- if (!_headersAttribute) {
- _headersAttribute = [NSMutableDictionary dictionary];
- }
- return _headersAttribute;
- }
- - (NSMutableDictionary *)footersAttribute {
- if (!_footersAttribute) {
- _footersAttribute = [NSMutableDictionary dictionary];
- }
- return _footersAttribute;
- }
- - (NSMutableArray *)unionRects {
- if (!_unionRects) {
- _unionRects = [NSMutableArray array];
- }
- return _unionRects;
- }
- - (NSMutableArray *)columnHeights {
- if (!_columnHeights) {
- _columnHeights = [NSMutableArray array];
- }
- return _columnHeights;
- }
- - (NSMutableArray *)allItemAttributes {
- if (!_allItemAttributes) {
- _allItemAttributes = [NSMutableArray array];
- }
- return _allItemAttributes;
- }
- - (NSMutableArray *)sectionItemAttributes {
- if (!_sectionItemAttributes) {
- _sectionItemAttributes = [NSMutableArray array];
- }
- return _sectionItemAttributes;
- }
- - (id <CHTCollectionViewDelegateWaterfallLayout> )delegate {
- return (id <CHTCollectionViewDelegateWaterfallLayout> )self.collectionView.delegate;
- }
- #pragma mark - Init
- - (void)commonInit {
- _columnCount = 2;
- _minimumColumnSpacing = 10;
- _minimumInteritemSpacing = 10;
- _headerHeight = 0;
- _footerHeight = 0;
- _sectionInset = UIEdgeInsetsZero;
- _headerInset = UIEdgeInsetsZero;
- _footerInset = UIEdgeInsetsZero;
- _itemRenderDirection = CHTCollectionViewWaterfallLayoutItemRenderDirectionShortestFirst;
- }
- - (id)init {
- if (self = [super init]) {
- [self commonInit];
- }
- return self;
- }
- - (id)initWithCoder:(NSCoder *)aDecoder {
- if (self = [super initWithCoder:aDecoder]) {
- [self commonInit];
- }
- return self;
- }
- #pragma mark - Methods to Override
- - (void)prepareLayout {
- [super prepareLayout];
- [self.headersAttribute removeAllObjects];
- [self.footersAttribute removeAllObjects];
- [self.unionRects removeAllObjects];
- [self.columnHeights removeAllObjects];
- [self.allItemAttributes removeAllObjects];
- [self.sectionItemAttributes removeAllObjects];
- NSInteger numberOfSections = [self.collectionView numberOfSections];
- if (numberOfSections == 0) {
- return;
- }
- NSAssert([self.delegate conformsToProtocol:@protocol(CHTCollectionViewDelegateWaterfallLayout)], @"UICollectionView's delegate should conform to CHTCollectionViewDelegateWaterfallLayout protocol");
- NSAssert(self.columnCount > 0 || [self.delegate respondsToSelector:@selector(collectionView:layout:columnCountForSection:)], @"UICollectionViewWaterfallLayout's columnCount should be greater than 0, or delegate must implement columnCountForSection:");
- // Initialize variables
- NSInteger idx = 0;
- for (NSInteger section = 0; section < numberOfSections; section++) {
- NSInteger columnCount = [self columnCountForSection:section];
- NSMutableArray *sectionColumnHeights = [NSMutableArray arrayWithCapacity:columnCount];
- for (idx = 0; idx < columnCount; idx++) {
- [sectionColumnHeights addObject:@(0)];
- }
- [self.columnHeights addObject:sectionColumnHeights];
- }
- // Create attributes
- CGFloat top = 0;
- UICollectionViewLayoutAttributes *attributes;
- for (NSInteger section = 0; section < numberOfSections; ++section) {
- /*
- * 1. Get section-specific metrics (minimumInteritemSpacing, sectionInset)
- */
- CGFloat minimumInteritemSpacing;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) {
- minimumInteritemSpacing = [self.delegate collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section];
- } else {
- minimumInteritemSpacing = self.minimumInteritemSpacing;
- }
- CGFloat columnSpacing = self.minimumColumnSpacing;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:minimumColumnSpacingForSectionAtIndex:)]) {
- columnSpacing = [self.delegate collectionView:self.collectionView layout:self minimumColumnSpacingForSectionAtIndex:section];
- }
- UIEdgeInsets sectionInset;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
- sectionInset = [self.delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:section];
- } else {
- sectionInset = self.sectionInset;
- }
- CGFloat width = self.collectionView.bounds.size.width - sectionInset.left - sectionInset.right;
- NSInteger columnCount = [self columnCountForSection:section];
- CGFloat itemWidth = CHTFloorCGFloat((width - (columnCount - 1) * columnSpacing) / columnCount);
- /*
- * 2. Section header
- */
- CGFloat headerHeight;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:heightForHeaderInSection:)]) {
- headerHeight = [self.delegate collectionView:self.collectionView layout:self heightForHeaderInSection:section];
- } else {
- headerHeight = self.headerHeight;
- }
- UIEdgeInsets headerInset;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:insetForHeaderInSection:)]) {
- headerInset = [self.delegate collectionView:self.collectionView layout:self insetForHeaderInSection:section];
- } else {
- headerInset = self.headerInset;
- }
- top += headerInset.top;
- if (headerHeight > 0) {
- attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
- attributes.frame = CGRectMake(headerInset.left,
- top,
- self.collectionView.bounds.size.width - (headerInset.left + headerInset.right),
- headerHeight);
- self.headersAttribute[@(section)] = attributes;
- [self.allItemAttributes addObject:attributes];
- top = CGRectGetMaxY(attributes.frame) + headerInset.bottom;
- }
- top += sectionInset.top;
- for (idx = 0; idx < columnCount; idx++) {
- self.columnHeights[section][idx] = @(top);
- }
- /*
- * 3. Section items
- */
- NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
- NSMutableArray *itemAttributes = [NSMutableArray arrayWithCapacity:itemCount];
- // Item will be put into shortest column.
- for (idx = 0; idx < itemCount; idx++) {
- NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section];
- NSUInteger columnIndex = [self nextColumnIndexForItem:idx inSection:section];
- CGFloat xOffset = sectionInset.left + (itemWidth + columnSpacing) * columnIndex;
- CGFloat yOffset = [self.columnHeights[section][columnIndex] floatValue];
- CGSize itemSize = [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
- CGFloat itemHeight = 0;
- if (itemSize.height > 0 && itemSize.width > 0) {
- itemHeight = CHTFloorCGFloat(itemSize.height * itemWidth / itemSize.width);
- }
- attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
- attributes.frame = CGRectMake(xOffset, yOffset, itemWidth, itemHeight);
- [itemAttributes addObject:attributes];
- [self.allItemAttributes addObject:attributes];
- self.columnHeights[section][columnIndex] = @(CGRectGetMaxY(attributes.frame) + minimumInteritemSpacing);
- }
- [self.sectionItemAttributes addObject:itemAttributes];
- /*
- * 4. Section footer
- */
- CGFloat footerHeight;
- NSUInteger columnIndex = [self longestColumnIndexInSection:section];
- if (((NSArray *)self.columnHeights[section]).count > 0) {
- top = [self.columnHeights[section][columnIndex] floatValue] - minimumInteritemSpacing + sectionInset.bottom;
- } else {
- top = 0;
- }
-
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:heightForFooterInSection:)]) {
- footerHeight = [self.delegate collectionView:self.collectionView layout:self heightForFooterInSection:section];
- } else {
- footerHeight = self.footerHeight;
- }
- UIEdgeInsets footerInset;
- if ([self.delegate respondsToSelector:@selector(collectionView:layout:insetForFooterInSection:)]) {
- footerInset = [self.delegate collectionView:self.collectionView layout:self insetForFooterInSection:section];
- } else {
- footerInset = self.footerInset;
- }
- top += footerInset.top;
- if (footerHeight > 0) {
- attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
- attributes.frame = CGRectMake(footerInset.left,
- top,
- self.collectionView.bounds.size.width - (footerInset.left + footerInset.right),
- footerHeight);
- self.footersAttribute[@(section)] = attributes;
- [self.allItemAttributes addObject:attributes];
- top = CGRectGetMaxY(attributes.frame) + footerInset.bottom;
- }
- for (idx = 0; idx < columnCount; idx++) {
- self.columnHeights[section][idx] = @(top);
- }
- } // end of for (NSInteger section = 0; section < numberOfSections; ++section)
- // Build union rects
- idx = 0;
- NSInteger itemCounts = [self.allItemAttributes count];
- while (idx < itemCounts) {
- CGRect unionRect = ((UICollectionViewLayoutAttributes *)self.allItemAttributes[idx]).frame;
- NSInteger rectEndIndex = MIN(idx + unionSize, itemCounts);
- for (NSInteger i = idx + 1; i < rectEndIndex; i++) {
- unionRect = CGRectUnion(unionRect, ((UICollectionViewLayoutAttributes *)self.allItemAttributes[i]).frame);
- }
- idx = rectEndIndex;
- [self.unionRects addObject:[NSValue valueWithCGRect:unionRect]];
- }
- }
- - (CGSize)collectionViewContentSize {
- NSInteger numberOfSections = [self.collectionView numberOfSections];
- if (numberOfSections == 0) {
- return CGSizeZero;
- }
- CGSize contentSize = self.collectionView.bounds.size;
- contentSize.height = [[[self.columnHeights lastObject] firstObject] floatValue];
- if (contentSize.height < self.minimumContentHeight) {
- contentSize.height = self.minimumContentHeight;
- }
- return contentSize;
- }
- - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
- if (path.section >= [self.sectionItemAttributes count]) {
- return nil;
- }
- if (path.item >= [self.sectionItemAttributes[path.section] count]) {
- return nil;
- }
- return (self.sectionItemAttributes[path.section])[path.item];
- }
- - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
- UICollectionViewLayoutAttributes *attribute = nil;
- if ([kind isEqualToString:CHTCollectionElementKindSectionHeader]) {
- attribute = self.headersAttribute[@(indexPath.section)];
- } else if ([kind isEqualToString:CHTCollectionElementKindSectionFooter]) {
- attribute = self.footersAttribute[@(indexPath.section)];
- }
- return attribute;
- }
- - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
- NSInteger i;
- NSInteger begin = 0, end = self.unionRects.count;
- NSMutableDictionary *cellAttrDict = [NSMutableDictionary dictionary];
- NSMutableDictionary *supplHeaderAttrDict = [NSMutableDictionary dictionary];
- NSMutableDictionary *supplFooterAttrDict = [NSMutableDictionary dictionary];
- NSMutableDictionary *decorAttrDict = [NSMutableDictionary dictionary];
- for (i = 0; i < self.unionRects.count; i++) {
- if (CGRectIntersectsRect(rect, [self.unionRects[i] CGRectValue])) {
- begin = i * unionSize;
- break;
- }
- }
- for (i = self.unionRects.count - 1; i >= 0; i--) {
- if (CGRectIntersectsRect(rect, [self.unionRects[i] CGRectValue])) {
- end = MIN((i + 1) * unionSize, self.allItemAttributes.count);
- break;
- }
- }
- for (i = begin; i < end; i++) {
- UICollectionViewLayoutAttributes *attr = self.allItemAttributes[i];
- if (CGRectIntersectsRect(rect, attr.frame)) {
- switch (attr.representedElementCategory) {
- case UICollectionElementCategorySupplementaryView:
- if ([attr.representedElementKind isEqualToString:CHTCollectionElementKindSectionHeader]) {
- supplHeaderAttrDict[attr.indexPath] = attr;
- } else if ([attr.representedElementKind isEqualToString:CHTCollectionElementKindSectionFooter]) {
- supplFooterAttrDict[attr.indexPath] = attr;
- }
- break;
- case UICollectionElementCategoryDecorationView:
- decorAttrDict[attr.indexPath] = attr;
- break;
- case UICollectionElementCategoryCell:
- cellAttrDict[attr.indexPath] = attr;
- break;
- }
- }
- }
- NSArray *result = [cellAttrDict.allValues arrayByAddingObjectsFromArray:supplHeaderAttrDict.allValues];
- result = [result arrayByAddingObjectsFromArray:supplFooterAttrDict.allValues];
- result = [result arrayByAddingObjectsFromArray:decorAttrDict.allValues];
- return result;
- }
- - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
- CGRect oldBounds = self.collectionView.bounds;
- if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
- return YES;
- }
- return NO;
- }
- #pragma mark - Private Methods
- /**
- * Find the shortest column.
- *
- * @return index for the shortest column
- */
- - (NSUInteger)shortestColumnIndexInSection:(NSInteger)section {
- __block NSUInteger index = 0;
- __block CGFloat shortestHeight = MAXFLOAT;
- [self.columnHeights[section] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
- CGFloat height = [obj floatValue];
- if (height < shortestHeight) {
- shortestHeight = height;
- index = idx;
- }
- }];
- return index;
- }
- /**
- * Find the longest column.
- *
- * @return index for the longest column
- */
- - (NSUInteger)longestColumnIndexInSection:(NSInteger)section {
- __block NSUInteger index = 0;
- __block CGFloat longestHeight = 0;
- [self.columnHeights[section] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
- CGFloat height = [obj floatValue];
- if (height > longestHeight) {
- longestHeight = height;
- index = idx;
- }
- }];
- return index;
- }
- /**
- * Find the index for the next column.
- *
- * @return index for the next column
- */
- - (NSUInteger)nextColumnIndexForItem:(NSInteger)item inSection:(NSInteger)section {
- NSUInteger index = 0;
- NSInteger columnCount = [self columnCountForSection:section];
- switch (self.itemRenderDirection) {
- case CHTCollectionViewWaterfallLayoutItemRenderDirectionShortestFirst:
- index = [self shortestColumnIndexInSection:section];
- break;
- case CHTCollectionViewWaterfallLayoutItemRenderDirectionLeftToRight:
- index = (item % columnCount);
- break;
- case CHTCollectionViewWaterfallLayoutItemRenderDirectionRightToLeft:
- index = (columnCount - 1) - (item % columnCount);
- break;
- default:
- index = [self shortestColumnIndexInSection:section];
- break;
- }
- return index;
- }
- @end
|