From eebdaffab4405e1d56f8f138cfd30e6d475a4129 Mon Sep 17 00:00:00 2001 From: Nelson Tai Date: Wed, 5 Feb 2014 00:03:06 +0800 Subject: [PATCH 1/6] [Fix] shouldInvalidateLayoutForBoundsChange --- CHTCollectionViewWaterfallLayout.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHTCollectionViewWaterfallLayout.m b/CHTCollectionViewWaterfallLayout.m index be1813e..71a489c 100644 --- a/CHTCollectionViewWaterfallLayout.m +++ b/CHTCollectionViewWaterfallLayout.m @@ -267,6 +267,11 @@ - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { + CGRect oldBounds = self.collectionView.bounds; + if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds) || + CGRectGetHeight(newBounds) != CGRectGetHeight(oldBounds)) { + return YES; + } return NO; } From 36202fc669a15e04b4143ad76ea7174613d1663e Mon Sep 17 00:00:00 2001 From: Andreas Maechler Date: Thu, 6 Feb 2014 10:26:16 -0800 Subject: [PATCH 2/6] Call [self commonInit] also in initWithCoder: When using Nibs / storyboards for initializing the layout, initWithCoder is the designated initializer. --- CHTCollectionViewWaterfallLayout.m | 36 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/CHTCollectionViewWaterfallLayout.m b/CHTCollectionViewWaterfallLayout.m index be1813e..8aa6569 100644 --- a/CHTCollectionViewWaterfallLayout.m +++ b/CHTCollectionViewWaterfallLayout.m @@ -81,20 +81,32 @@ - (void)setVerticalItemSpacing:(CGFloat)verticalItemSpacing { } #pragma mark - Init -- (void)commonInit { - _columnCount = 2; - _itemWidth = 140; - _headerHeight = 0; - _footerHeight = 0; - _verticalItemSpacing = 0; - _sectionInset = UIEdgeInsetsZero; -} - (id)init { - if (self = [super init]) { - [self commonInit]; - } - return self; + self = [super init]; + if (self) { + [self commonInit]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self) { + [self commonInit]; + } + + return self; +} + +- (void)commonInit { + _columnCount = 2; + _itemWidth = 140; + _headerHeight = 0; + _footerHeight = 0; + _verticalItemSpacing = 0; + _sectionInset = UIEdgeInsetsZero; } #pragma mark - Methods to Override From c6544f4f871c8512810be6b54fc434fd1f172cf2 Mon Sep 17 00:00:00 2001 From: Nelson Tai Date: Sun, 9 Feb 2014 00:54:55 +0800 Subject: [PATCH 3/6] Section header and/or footer should work properly --- CHTCollectionViewWaterfallLayout.h | 18 ++-- CHTCollectionViewWaterfallLayout.m | 160 +++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 52 deletions(-) diff --git a/CHTCollectionViewWaterfallLayout.h b/CHTCollectionViewWaterfallLayout.h index a4d08b0..9e9167b 100644 --- a/CHTCollectionViewWaterfallLayout.h +++ b/CHTCollectionViewWaterfallLayout.h @@ -7,9 +7,13 @@ #import -#pragma mark - Constants +/** + * Constants that specify the types of supplementary views that can be presented using a waterfall layout. + */ +/// A supplementary view that identifies the header for a given section. extern NSString *const CHTCollectionElementKindSectionHeader; +/// A supplementary view that identifies the footer for a given section. extern NSString *const CHTCollectionElementKindSectionFooter; #pragma mark - CHTCollectionViewDelegateWaterfallLayout @@ -31,9 +35,7 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * @return * The height of the specified item. Must be greater than 0. */ -- (CGFloat)collectionView:(UICollectionView *)collectionView - layout:(UICollectionViewLayout *)collectionViewLayout - heightForItemAtIndexPath:(NSIndexPath *)indexPath; +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForItemAtIndexPath:(NSIndexPath *)indexPath; @optional /** @@ -55,9 +57,7 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * @see * headerHeight */ -- (CGFloat)collectionView:(UICollectionView *)collectionView - layout:(UICollectionViewLayout *)collectionViewLayout - heightForHeaderInSection:(NSInteger)section; +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForHeaderInSection:(NSInteger)section; /** * Asks the delegate for the height of the footer view in the specified section. @@ -78,9 +78,7 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * @see * footerHeight */ -- (CGFloat)collectionView:(UICollectionView *)collectionView - layout:(UICollectionViewLayout *)collectionViewLayout - heightForFooterInSection:(NSInteger)section; +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForFooterInSection:(NSInteger)section; @end #pragma mark - CHTCollectionViewWaterfallLayout diff --git a/CHTCollectionViewWaterfallLayout.m b/CHTCollectionViewWaterfallLayout.m index 71a489c..56f2a3d 100644 --- a/CHTCollectionViewWaterfallLayout.m +++ b/CHTCollectionViewWaterfallLayout.m @@ -11,19 +11,18 @@ NSString *const CHTCollectionElementKindSectionFooter = @"CHTCollectionElementKindSectionFooter"; @interface CHTCollectionViewWaterfallLayout () -@property (nonatomic, weak) id delegate; +/// The delegate will point to collectionView.delegate automatically. +@property (nonatomic, weak) id delegate; /// Array to store height for each column @property (nonatomic, strong) NSMutableArray *columnHeights; -/// Array of arrays -/// Each array stores item attributes for each section +/// Array of arrays. Each array stores item attributes for each section @property (nonatomic, strong) NSMutableArray *sectionItemAttributes; -/// Array to store attributes for all items -/// Include headers, cells, and footers +/// Array to store attributes for all items includes headers, cells, and footers @property (nonatomic, strong) NSMutableArray *allItemAttributes; -/// Array to store attributes for headers -@property (nonatomic, strong) NSMutableArray *headersAttributes; -/// Array to store attributes for footers -@property (nonatomic, strong) NSMutableArray *footersAttributes; +/// Dictionary to store section headers' attribute +@property (nonatomic, strong) NSMutableDictionary *headersAttribute; +/// Dictionary to store section footers' attribute +@property (nonatomic, strong) NSMutableDictionary *footersAttrubite; /// Array to store union rectangles @property (nonatomic, strong) NSMutableArray *unionRects; /// Inter spacing at X-axis for each item @@ -80,6 +79,48 @@ - (void)setVerticalItemSpacing:(CGFloat)verticalItemSpacing { } } +- (NSMutableDictionary *)headersAttribute { + if (!_headersAttribute) { + _headersAttribute = [NSMutableDictionary dictionary]; + } + return _headersAttribute; +} + +- (NSMutableDictionary *)footersAttrubite { + if (!_footersAttrubite) { + _footersAttrubite = [NSMutableDictionary dictionary]; + } + return _footersAttrubite; +} + +- (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; +} + #pragma mark - Init - (void)commonInit { _columnCount = 2; @@ -88,6 +129,7 @@ - (void)commonInit { _footerHeight = 0; _verticalItemSpacing = 0; _sectionInset = UIEdgeInsetsZero; + _delegate = (id )self.collectionView.delegate; } - (id)init { @@ -123,13 +165,12 @@ - (void)prepareLayout { self.interItemSpacingY = self.interItemSpacingX; } - self.delegate = (id)self.collectionView.delegate; - self.headersAttributes = [NSMutableArray array]; - self.footersAttributes = [NSMutableArray array]; - self.unionRects = [NSMutableArray array]; - self.columnHeights = [NSMutableArray array]; - self.allItemAttributes = [NSMutableArray array]; - self.sectionItemAttributes = [NSMutableArray array]; + [self.headersAttribute removeAllObjects]; + [self.footersAttrubite removeAllObjects]; + [self.unionRects removeAllObjects]; + [self.columnHeights removeAllObjects]; + [self.allItemAttributes removeAllObjects]; + [self.sectionItemAttributes removeAllObjects]; for (idx = 0; idx < self.columnCount; idx++) { [self.columnHeights addObject:@(0)]; @@ -138,26 +179,40 @@ - (void)prepareLayout { // Create attributes CGFloat sectionTop = self.sectionInset.top; UICollectionViewLayoutAttributes *attributes; - for (NSInteger section = 0; section < numberOfSections; ++section) { - // 1. Section header - attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + for (NSInteger section = 0; section < numberOfSections; ++section) { + /* + * 1. Section header + */ + CGFloat headerHeight; if ([self.delegate respondsToSelector:@selector(collectionView:layout:heightForHeaderInSection:)]) { - CGFloat headerHeight = [self.delegate collectionView:self.collectionView layout:self heightForHeaderInSection:section]; - attributes.frame = CGRectMake(self.sectionInset.left, sectionTop, width, headerHeight); + headerHeight = [self.delegate collectionView:self.collectionView layout:self heightForHeaderInSection:section]; } else { - attributes.frame = CGRectMake(self.sectionInset.left, sectionTop, width, self.headerHeight); + headerHeight = self.headerHeight; } - [self.headersAttributes addObject:attributes]; - [self.allItemAttributes addObject:attributes]; - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(CGRectGetMaxY(attributes.frame) + self.interItemSpacingY); + if (headerHeight > 0) { + attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + attributes.frame = CGRectMake(self.sectionInset.left, sectionTop, width, headerHeight); + + self.headersAttribute[@(section)] = attributes; + [self.allItemAttributes addObject:attributes]; + + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(CGRectGetMaxY(attributes.frame) + self.interItemSpacingY); + } + } else { + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(sectionTop); + } } - // 2. Section items + /* + * 2. 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]; @@ -175,23 +230,35 @@ - (void)prepareLayout { [self.sectionItemAttributes addObject:itemAttributes]; - // 3. Section footer + /* + * Section footer + */ NSUInteger columnIndex = [self longestColumnIndex]; CGFloat yOffset = [self.columnHeights[columnIndex] floatValue]; - attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + CGFloat footerHeight; if ([self.delegate respondsToSelector:@selector(collectionView:layout:heightForFooterInSection:)]) { - CGFloat footerHeight = [self.delegate collectionView:self.collectionView layout:self heightForFooterInSection:section]; - attributes.frame = CGRectMake(self.sectionInset.left, yOffset, width, footerHeight); + footerHeight = [self.delegate collectionView:self.collectionView layout:self heightForFooterInSection:section]; } else { - attributes.frame = CGRectMake(self.sectionInset.left, yOffset, width, self.footerHeight); + footerHeight = self.footerHeight; } - [self.footersAttributes addObject:attributes]; - [self.allItemAttributes addObject:attributes]; + + if (footerHeight > 0) { + attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; + attributes.frame = CGRectMake(self.sectionInset.left, yOffset, width, footerHeight); + + self.footersAttrubite[@(section)] = attributes; + [self.allItemAttributes addObject:attributes]; - sectionTop = CGRectGetMaxY(attributes.frame) + self.interItemSpacingY; - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(sectionTop); + sectionTop = CGRectGetMaxY(attributes.frame) + self.interItemSpacingY; + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(sectionTop); + } + } else { + sectionTop = yOffset + self.interItemSpacingY; + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(sectionTop); + } } } // end of for (NSInteger section = 0; section < numberOfSections; ++section) @@ -231,12 +298,13 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSInde } - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + UICollectionViewLayoutAttributes *attribute = nil; if ([kind isEqualToString:CHTCollectionElementKindSectionHeader]) { - return self.headersAttributes[indexPath.section]; + attribute = self.headersAttribute[@(indexPath.section)]; } else if ([kind isEqualToString:CHTCollectionElementKindSectionFooter]) { - return self.footersAttributes[indexPath.section]; + attribute = self.footersAttrubite[@(indexPath.section)]; } - return nil; + return attribute; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { @@ -277,7 +345,11 @@ - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { #pragma mark - Private Methods -// Find the shortest column. +/** + * Find the shortest column. + * + * @return index for the shortest column + */ - (NSUInteger)shortestColumnIndex { __block NSUInteger index = 0; __block CGFloat shortestHeight = MAXFLOAT; @@ -293,7 +365,11 @@ - (NSUInteger)shortestColumnIndex { return index; } -// Find the longest column. +/** + * Find the longest column. + * + * @return index for the longest column + */ - (NSUInteger)longestColumnIndex { __block NSUInteger index = 0; __block CGFloat longestHeight = 0; From f0561f2012f3d97c50e43fd77aa0bd94e8669caf Mon Sep 17 00:00:00 2001 From: Andreas Maechler Date: Sat, 8 Feb 2014 11:05:00 -0800 Subject: [PATCH 4/6] Call [super initWithCoder] instead of init --- CHTCollectionViewWaterfallLayout.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHTCollectionViewWaterfallLayout.m b/CHTCollectionViewWaterfallLayout.m index 8aa6569..d1f1747 100644 --- a/CHTCollectionViewWaterfallLayout.m +++ b/CHTCollectionViewWaterfallLayout.m @@ -92,7 +92,7 @@ - (id)init { } - (id)initWithCoder:(NSCoder *)aDecoder { - self = [super init]; + self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } From 3d6da7fee212d8683198ba9c261a9a655e2633ba Mon Sep 17 00:00:00 2001 From: Nelson Tai Date: Sun, 9 Feb 2014 16:01:26 +0800 Subject: [PATCH 5/6] Change some properties and methods to imitate UICollectionViewFlowLayout's usage as much as possible - Don't need to calculate item size by yourself. - Also fix some issues with frame calculation. --- CHTCollectionViewWaterfallLayout.h | 59 +++++++++++++---- CHTCollectionViewWaterfallLayout.m | 100 ++++++++++++----------------- Demo/Demo/ViewController.h | 1 - Demo/Demo/ViewController.m | 64 ++++++++---------- README.md | 20 +++--- 5 files changed, 126 insertions(+), 118 deletions(-) diff --git a/CHTCollectionViewWaterfallLayout.h b/CHTCollectionViewWaterfallLayout.h index 9e9167b..2dba061 100644 --- a/CHTCollectionViewWaterfallLayout.h +++ b/CHTCollectionViewWaterfallLayout.h @@ -20,10 +20,18 @@ extern NSString *const CHTCollectionElementKindSectionFooter; @class CHTCollectionViewWaterfallLayout; +/** + * The CHTCollectionViewDelegateWaterfallLayout protocol defines methods that let you coordinate with a + * CHTCollectionViewWaterfallLayout object to implement a waterfall-based layout. + * The methods of this protocol define the size of items. + * + * The waterfall layout object expects the collection view’s delegate object to adopt this protocol. + * Therefore, implement this protocol on object assigned to your collection view’s delegate property. + */ @protocol CHTCollectionViewDelegateWaterfallLayout @required /** - * Asks the delegate for the height of the specified item’s cell. + * Asks the delegate for the size of the specified item’s cell. * * @param collectionView * The collection view object displaying the waterfall layout. @@ -33,9 +41,9 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * The index path of the item. * * @return - * The height of the specified item. Must be greater than 0. + * The original size of the specified item. Both width and height must be greater than 0. */ -- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForItemAtIndexPath:(NSIndexPath *)indexPath; +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; @optional /** @@ -73,7 +81,7 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * The height of the footer. If you return 0, no footer is added. * * @discussion - * If you do not implement this method, the waterfall layout uses the value in its footerHeight property to set the size of the header. + * If you do not implement this method, the waterfall layout uses the value in its footerHeight property to set the size of the footer. * * @see * footerHeight @@ -83,7 +91,22 @@ extern NSString *const CHTCollectionElementKindSectionFooter; #pragma mark - CHTCollectionViewWaterfallLayout +/** + * The CHTCollectionViewWaterfallLayout class is a concrete layout object that organizes items into waterfall-based grids + * with optional header and footer views for each section. + * + * A waterfall layout works with the collection view’s delegate object to determine the size of items, headers, and footers + * in each section. That delegate object must conform to the `CHTCollectionViewDelegateWaterfallLayout` protocol. + * + * Each section in a waterfall layout can have its own custom header and footer. To configure the header or footer for a view, + * you must configure the height of the header or footer to be non zero. You can do this by implementing the appropriate delegate + * methods or by assigning appropriate values to the `headerHeight` and `footerHeight` properties. + * If the header or footer height is 0, the corresponding view is not added to the collection view. + * + * @note CHTCollectionViewWaterfallLayout doesn't support decoration view, and it supports vertical scrolling direction only. + */ @interface CHTCollectionViewWaterfallLayout : UICollectionViewLayout + /** * @brief How many columns for this layout. * @discussion Default: 2 @@ -91,16 +114,24 @@ extern NSString *const CHTCollectionElementKindSectionFooter; @property (nonatomic, assign) NSInteger columnCount; /** - * @brief Width for each item. - * @discussion Default: 140 + * @brief The minimum spacing to use between successive columns. + * @discussion Default: 10.0 */ -@property (nonatomic, assign) CGFloat itemWidth; +@property (nonatomic, assign) CGFloat minimumColumnSpacing; + +/** + * @brief The minimum spacing to use between items in the same column. + * @discussion Default: 10.0 + * @note This spacing is not applied to the space between header and columns or between columns and footer. + */ +@property (nonatomic, assign) CGFloat minimumInteritemSpacing; /** * @brief Height for section header * @discussion * If your collectionView's delegate doesn't implement `collectionView:layout:heightForHeaderInSection:`, * then this value will be used. + * * Default: 0 */ @property (nonatomic, assign) CGFloat headerHeight; @@ -110,19 +141,19 @@ extern NSString *const CHTCollectionElementKindSectionFooter; * @discussion * If your collectionView's delegate doesn't implement `collectionView:layout:heightForFooterInSection:`, * then this value will be used. + * * Default: 0 */ @property (nonatomic, assign) CGFloat footerHeight; /** * @brief The margins that are used to lay out content in each section. - * @discussion Default: UIEdgeInsetsZero + * @discussion + * Section insets are margins applied only to the items in the section. + * They represent the distance between the header view and the columns and between the columns and the footer view. + * They also indicate the spacing on either side of columns. They do not affect the size of the headers or footers themselves. + * + * Default: UIEdgeInsetsZero */ @property (nonatomic, assign) UIEdgeInsets sectionInset; - -/** - * @brief Spacing between items vertically. - * @discussion It will be calculated automatically if you don't assign a value. - */ -@property (nonatomic, assign) CGFloat verticalItemSpacing; @end diff --git a/CHTCollectionViewWaterfallLayout.m b/CHTCollectionViewWaterfallLayout.m index 91371d8..5a966ee 100644 --- a/CHTCollectionViewWaterfallLayout.m +++ b/CHTCollectionViewWaterfallLayout.m @@ -11,7 +11,7 @@ NSString *const CHTCollectionElementKindSectionFooter = @"CHTCollectionElementKindSectionFooter"; @interface CHTCollectionViewWaterfallLayout () -/// The delegate will point to collectionView.delegate automatically. +/// The delegate will point to collection view's delegate automatically. @property (nonatomic, weak) id delegate; /// Array to store height for each column @property (nonatomic, strong) NSMutableArray *columnHeights; @@ -25,10 +25,8 @@ @interface CHTCollectionViewWaterfallLayout () @property (nonatomic, strong) NSMutableDictionary *footersAttrubite; /// Array to store union rectangles @property (nonatomic, strong) NSMutableArray *unionRects; -/// Inter spacing at X-axis for each item -@property (nonatomic, assign) CGFloat interItemSpacingX; -/// Inter spacing at Y-axis for each item -@property (nonatomic, assign) CGFloat interItemSpacingY; +/// Width for each item +@property (nonatomic, assign) CGFloat itemWidth; @end @implementation CHTCollectionViewWaterfallLayout @@ -36,7 +34,7 @@ @implementation CHTCollectionViewWaterfallLayout /// How many items to be union into a single rectangle const NSInteger unionSize = 20; -#pragma mark - Accessors +#pragma mark - Public Accessors - (void)setColumnCount:(NSInteger)columnCount { if (_columnCount != columnCount) { _columnCount = columnCount; @@ -44,9 +42,16 @@ - (void)setColumnCount:(NSInteger)columnCount { } } -- (void)setItemWidth:(CGFloat)itemWidth { - if (_itemWidth != itemWidth) { - _itemWidth = itemWidth; +- (void)setMinimumColumnSpacing:(CGFloat)minimumColumnSpacing { + if (_minimumColumnSpacing != minimumColumnSpacing) { + _minimumColumnSpacing = minimumColumnSpacing; + [self invalidateLayout]; + } +} + +- (void)setMinimumInteritemSpacing:(CGFloat)minimumInteritemSpacing { + if (_minimumInteritemSpacing != minimumInteritemSpacing) { + _minimumInteritemSpacing = minimumInteritemSpacing; [self invalidateLayout]; } } @@ -72,13 +77,7 @@ - (void)setSectionInset:(UIEdgeInsets)sectionInset { } } -- (void)setVerticalItemSpacing:(CGFloat)verticalItemSpacing { - if (_verticalItemSpacing != verticalItemSpacing) { - _verticalItemSpacing = verticalItemSpacing; - [self invalidateLayout]; - } -} - +#pragma mark - Private Accessors - (NSMutableDictionary *)headersAttribute { if (!_headersAttribute) { _headersAttribute = [NSMutableDictionary dictionary]; @@ -124,19 +123,17 @@ - (NSMutableArray *)sectionItemAttributes { #pragma mark - Init - (void)commonInit { _columnCount = 2; - _itemWidth = 140; + _minimumColumnSpacing = 10; + _minimumInteritemSpacing = 10; _headerHeight = 0; _footerHeight = 0; - _verticalItemSpacing = 0; _sectionInset = UIEdgeInsetsZero; - _delegate = (id )self.collectionView.delegate; } - (id)init { if (self = [super init]) { [self commonInit]; } - return self; } @@ -144,7 +141,6 @@ - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self commonInit]; } - return self; } @@ -157,22 +153,15 @@ - (void)prepareLayout { return; } - NSAssert(self.columnCount > 0, @"columnCount for UICollectionViewWaterfallLayout should be greater than 0."); + self.delegate = (id )self.collectionView.delegate; + NSAssert([self.delegate conformsToProtocol:@protocol(CHTCollectionViewDelegateWaterfallLayout)], @"UICollectionView's delegate should conform to CHTCollectionViewDelegateWaterfallLayout protocol"); + NSAssert(self.columnCount > 0, @"UICollectionViewWaterfallLayout's columnCount should be greater than 0"); // Initialize variables NSInteger idx = 0; CGFloat width = self.collectionView.frame.size.width - self.sectionInset.left - self.sectionInset.right; - if (self.columnCount > 1) { - self.interItemSpacingX = floorf((width - self.columnCount * self.itemWidth) / (self.columnCount - 1)); - } else { - self.interItemSpacingX = floorf((width - self.itemWidth) / 2); - } - if (self.verticalItemSpacing > 0) { - self.interItemSpacingY = self.verticalItemSpacing; - } else { - self.interItemSpacingY = self.interItemSpacingX; - } + self.itemWidth = floorf((width - (self.columnCount - 1) * self.minimumColumnSpacing) / self.columnCount); [self.headersAttribute removeAllObjects]; [self.footersAttrubite removeAllObjects]; @@ -186,7 +175,7 @@ - (void)prepareLayout { } // Create attributes - CGFloat sectionTop = self.sectionInset.top; + CGFloat top = 0; UICollectionViewLayoutAttributes *attributes; for (NSInteger section = 0; section < numberOfSections; ++section) { @@ -202,18 +191,17 @@ - (void)prepareLayout { if (headerHeight > 0) { attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; - attributes.frame = CGRectMake(self.sectionInset.left, sectionTop, width, headerHeight); + attributes.frame = CGRectMake(0, top, self.collectionView.frame.size.width, headerHeight); self.headersAttribute[@(section)] = attributes; [self.allItemAttributes addObject:attributes]; - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(CGRectGetMaxY(attributes.frame) + self.interItemSpacingY); - } - } else { - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(sectionTop); - } + top = CGRectGetMaxY(attributes.frame); + } + + top += self.sectionInset.top; + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(top); } /* @@ -225,16 +213,17 @@ - (void)prepareLayout { // Item will be put into shortest column. for (idx = 0; idx < itemCount; idx++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; - CGFloat itemHeight = [self.delegate collectionView:self.collectionView layout:self heightForItemAtIndexPath:indexPath]; + CGSize itemSize = [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath]; + CGFloat itemHeight = floorf(itemSize.height * self.itemWidth / itemSize.width); NSUInteger columnIndex = [self shortestColumnIndex]; - CGFloat xOffset = self.sectionInset.left + (self.itemWidth + self.interItemSpacingX) * columnIndex; + CGFloat xOffset = self.sectionInset.left + (self.itemWidth + self.minimumColumnSpacing) * columnIndex; CGFloat yOffset = [self.columnHeights[columnIndex] floatValue]; attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; attributes.frame = CGRectMake(xOffset, yOffset, self.itemWidth, itemHeight); [itemAttributes addObject:attributes]; [self.allItemAttributes addObject:attributes]; - self.columnHeights[columnIndex] = @(yOffset + itemHeight + self.interItemSpacingY); + self.columnHeights[columnIndex] = @(CGRectGetMaxY(attributes.frame) + self.minimumInteritemSpacing); } [self.sectionItemAttributes addObject:itemAttributes]; @@ -242,9 +231,9 @@ - (void)prepareLayout { /* * Section footer */ - NSUInteger columnIndex = [self longestColumnIndex]; - CGFloat yOffset = [self.columnHeights[columnIndex] floatValue]; CGFloat footerHeight; + NSUInteger columnIndex = [self longestColumnIndex]; + top = [self.columnHeights[columnIndex] floatValue] - self.minimumInteritemSpacing + self.sectionInset.bottom; if ([self.delegate respondsToSelector:@selector(collectionView:layout:heightForFooterInSection:)]) { footerHeight = [self.delegate collectionView:self.collectionView layout:self heightForFooterInSection:section]; @@ -254,20 +243,16 @@ - (void)prepareLayout { if (footerHeight > 0) { attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:CHTCollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; - attributes.frame = CGRectMake(self.sectionInset.left, yOffset, width, footerHeight); + attributes.frame = CGRectMake(0, top, self.collectionView.frame.size.width, footerHeight); self.footersAttrubite[@(section)] = attributes; [self.allItemAttributes addObject:attributes]; - sectionTop = CGRectGetMaxY(attributes.frame) + self.interItemSpacingY; - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(sectionTop); - } - } else { - sectionTop = yOffset + self.interItemSpacingY; - for (idx = 0; idx < self.columnCount; idx++) { - self.columnHeights[idx] = @(sectionTop); - } + top = CGRectGetMaxY(attributes.frame); + } + + for (idx = 0; idx < self.columnCount; idx++) { + self.columnHeights[idx] = @(top); } } // end of for (NSInteger section = 0; section < numberOfSections; ++section) @@ -290,8 +275,7 @@ - (CGSize)collectionViewContentSize { } CGSize contentSize = self.collectionView.bounds.size; - CGFloat height = [self.columnHeights[0] floatValue]; - contentSize.height = height - self.interItemSpacingY + self.sectionInset.bottom; + contentSize.height = [self.columnHeights[0] floatValue]; return contentSize; } diff --git a/Demo/Demo/ViewController.h b/Demo/Demo/ViewController.h index 6d3782c..0106a90 100644 --- a/Demo/Demo/ViewController.h +++ b/Demo/Demo/ViewController.h @@ -11,5 +11,4 @@ @interface ViewController : UIViewController @property (nonatomic, strong) IBOutlet UICollectionView *collectionView; -@property (nonatomic) CGFloat cellWidth; @end diff --git a/Demo/Demo/ViewController.m b/Demo/Demo/ViewController.m index e7408b7..622982e 100644 --- a/Demo/Demo/ViewController.m +++ b/Demo/Demo/ViewController.m @@ -11,34 +11,28 @@ #import "CHTCollectionViewWaterfallHeader.h" #import "CHTCollectionViewWaterfallFooter.h" -#define CELL_WIDTH 140 -#define CELL_COUNT 50 +#define CELL_COUNT 30 #define CELL_IDENTIFIER @"WaterfallCell" #define HEADER_IDENTIFIER @"WaterfallHeader" #define FOOTER_IDENTIFIER @"WaterfallFooter" @interface ViewController () -@property (nonatomic, strong) NSMutableArray *cellHeights; +@property (nonatomic, strong) NSMutableArray *cellSizes; @end @implementation ViewController -- (id)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - self.cellWidth = CELL_WIDTH; // Default if not setting runtime attribute - } - return self; -} - #pragma mark - Accessors + - (UICollectionView *)collectionView { if (!_collectionView) { CHTCollectionViewWaterfallLayout *layout = [[CHTCollectionViewWaterfallLayout alloc] init]; - layout.sectionInset = UIEdgeInsetsMake(9, 9, 9, 9); - layout.verticalItemSpacing = 5; + layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10); layout.headerHeight = 15; layout.footerHeight = 10; + layout.minimumColumnSpacing = 20; + layout.minimumInteritemSpacing = 30; _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; _collectionView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; @@ -57,17 +51,19 @@ - (UICollectionView *)collectionView { return _collectionView; } -- (NSMutableArray *)cellHeights { - if (!_cellHeights) { - _cellHeights = [NSMutableArray arrayWithCapacity:CELL_COUNT]; +- (NSMutableArray *)cellSizes { + if (!_cellSizes) { + _cellSizes = [NSMutableArray array]; for (NSInteger i = 0; i < CELL_COUNT; i++) { - _cellHeights[i] = @(arc4random() % 100 * 2 + 100); + CGSize size = CGSizeMake(arc4random() % 50 + 50, arc4random() % 50 + 50); + _cellSizes[i] = [NSValue valueWithCGSize:size]; } } - return _cellHeights; + return _cellSizes; } #pragma mark - Life Cycle + - (void)dealloc { _collectionView.delegate = nil; _collectionView.dataSource = nil; @@ -80,24 +76,22 @@ - (void)viewDidLoad { - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - [self updateLayout]; + [self updateLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation]; } -- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation - duration:(NSTimeInterval)duration { - [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation - duration:duration]; - [self updateLayout]; +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; + [self updateLayoutForOrientation:toInterfaceOrientation]; } -- (void)updateLayout { +- (void)updateLayoutForOrientation:(UIInterfaceOrientation)orientation { CHTCollectionViewWaterfallLayout *layout = - (CHTCollectionViewWaterfallLayout *)self.collectionView.collectionViewLayout; - layout.columnCount = self.collectionView.bounds.size.width / self.cellWidth; - layout.itemWidth = self.cellWidth; + (CHTCollectionViewWaterfallLayout *)self.collectionView.collectionViewLayout; + layout.columnCount = UIInterfaceOrientationIsPortrait(orientation) ? 2 : 3; } #pragma mark - UICollectionViewDataSource + - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return CELL_COUNT; } @@ -108,15 +102,13 @@ - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { CHTCollectionViewWaterfallCell *cell = - (CHTCollectionViewWaterfallCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CELL_IDENTIFIER - forIndexPath:indexPath]; + (CHTCollectionViewWaterfallCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CELL_IDENTIFIER + forIndexPath:indexPath]; cell.displayString = [NSString stringWithFormat:@"%d", indexPath.item]; return cell; } -- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView - viewForSupplementaryElementOfKind:(NSString *)kind - atIndexPath:(NSIndexPath *)indexPath { +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { UICollectionReusableView *reusableView = nil; if ([kind isEqualToString:CHTCollectionElementKindSectionHeader]) { @@ -132,11 +124,9 @@ - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView return reusableView; } -#pragma mark - UICollectionViewWaterfallLayoutDelegate -- (CGFloat) collectionView:(UICollectionView *)collectionView - layout:(CHTCollectionViewWaterfallLayout *)collectionViewLayout - heightForItemAtIndexPath:(NSIndexPath *)indexPath { - return [self.cellHeights[indexPath.item] floatValue]; +#pragma mark - CHTCollectionViewDelegateWaterfallLayout +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + return [self.cellSizes[indexPath.item] CGSizeValue]; } @end diff --git a/README.md b/README.md index fe450a2..1308030 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ CHTCollectionViewWaterfallLayout [![Version](https://cocoapod-badges.herokuapp.com/v/CHTCollectionViewWaterfallLayout/badge.png)](http://cocoadocs.org/docsets/CHTCollectionViewWaterfallLayout) [![Platform](https://cocoapod-badges.herokuapp.com/p/CHTCollectionViewWaterfallLayout/badge.png)](http://cocoadocs.org/docsets/CHTCollectionViewWaterfallLayout) -iOS 6 introduced a new feature called [UICollectionView]. **CHTCollectionViewWaterfallLayout** is a subclass of [UICollectionViewLayout], and it trys to imitate [UICollectionViewFlowLayout]'s usage as much as possible. +**CHTCollectionViewWaterfallLayout** is a subclass of [UICollectionViewLayout], and it trys to imitate [UICollectionViewFlowLayout]'s usage as much as possible. This layout is inspired by [Pinterest]. It also is compatible with [PSTCollectionView]. @@ -25,24 +25,22 @@ How to Use Read the demo codes and `CHTCollectionViewWaterfallLayout.h` header file for more information. #### Step 1 -There are some properties for you. Although they have default values, I strongly recommand you to set up `columnCount` and `itemWidth` to suit your needs. +There are some properties for you. Although they have default values, I strongly recommand you to set up `columnCount` to suit your needs. ``` objc @property (nonatomic, assign) NSInteger columnCount; -@property (nonatomic, assign) CGFloat itemWidth; +@property (nonatomic, assign) CGFloat minimumColumnSpacing; +@property (nonatomic, assign) CGFloat minimumInteritemSpacing; @property (nonatomic, assign) CGFloat headerHeight; @property (nonatomic, assign) CGFloat footerHeight; @property (nonatomic, assign) UIEdgeInsets sectionInset; -@property (nonatomic, assign) CGFloat verticalItemSpacically ``` #### Step 2 Your collectionView's delegate (which often is your view controller) must conforms to `CHTCollectionViewDelegateWaterfallLayout` protocol and implements the required method: ``` objc -- (CGFloat)collectionView:(UICollectionView *)collectionView - layout:(UICollectionViewLayout *)collectionViewLayout - heightForItemAtIndexPath:(NSIndexPath *)indexPath; +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; ``` #### Step 3 (Optional) @@ -63,6 +61,13 @@ CHTCollectionViewWaterfallLayout is available under the MIT license. See the LIC Changelog --------- +#### 0.0.6 +* [Add] Add `minimumColumnSpacing` and `minimumInteritemSpacing` properties. +* [Remove] Remove `itemWidth` property. The layout object will calculate a proper item width automatically. +* [Change] Rename delegate method `- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForItemAtIndexPath:(NSIndexPath *)indexPath` to `- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath`. It should return original size for each item. +* [Fix] Section header and/or footer should work properly. +* [Fix] Use `sectionInset` correctly. + #### 0.0.5 * [Add] Multiple sections. * [Add] Header and/or footer for section. @@ -70,7 +75,6 @@ Changelog * [Change] Remove `delegate` property, your collectionView's delegate **MUST** conforms to `` protocol. -[UICollectionView]: http://developer.apple.com/library/ios/#documentation/uikit/reference/UICollectionView_class/Reference/Reference.html [UICollectionViewLayout]: http://developer.apple.com/library/ios/#documentation/uikit/reference/UICollectionViewLayout_class/Reference/Reference.html [UICollectionViewFlowLayout]: https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewFlowLayout_class/Reference/Reference.html [Pinterest]: http://pinterest.com/ From c47a2c1fe2bcf748fd86a5de6dc28b0dcd793793 Mon Sep 17 00:00:00 2001 From: Nelson Tai Date: Mon, 17 Feb 2014 00:33:19 +0800 Subject: [PATCH 6/6] Update podspec and README --- CHTCollectionViewWaterfallLayout.podspec | 2 +- README.md | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHTCollectionViewWaterfallLayout.podspec b/CHTCollectionViewWaterfallLayout.podspec index 066ed39..39200d9 100644 --- a/CHTCollectionViewWaterfallLayout.podspec +++ b/CHTCollectionViewWaterfallLayout.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "CHTCollectionViewWaterfallLayout" - s.version = "0.5" + s.version = "0.6" s.summary = "The waterfall (i.e., Pinterest-like) layout for UICollectionView." s.homepage = "https://github.com/chiahsien/CHTCollectionViewWaterfallLayout" s.screenshots = "https://raw.github.com/chiahsien/UICollectionViewWaterfallLayout/master/Screenshots/2-columns.png", "https://raw.github.com/chiahsien/UICollectionViewWaterfallLayout/master/Screenshots/3-columns.png" diff --git a/README.md b/README.md index 1308030..28defda 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ Screen Shots ![2 columns](https://raw.github.com/chiahsien/UICollectionViewWaterfallLayout/master/Screenshots/2-columns.png) ![3 columns](https://raw.github.com/chiahsien/UICollectionViewWaterfallLayout/master/Screenshots/3-columns.png) +Features +-------- +* Easy to use, it trys to imitate [UICollectionViewFlowLayout]'s usage as much as possible. +* Highly customizable. +* Outstanding performance, try 10,000+ items and see the smoothness for yourself. +* Support header and footer views. + Prerequisite ------------ * ARC @@ -25,7 +32,7 @@ How to Use Read the demo codes and `CHTCollectionViewWaterfallLayout.h` header file for more information. #### Step 1 -There are some properties for you. Although they have default values, I strongly recommand you to set up `columnCount` to suit your needs. +Below lists the properties for you to customize the layout. Although they have default values, I strongly recommand you to set up at least the `columnCount` property to suit your needs. ``` objc @property (nonatomic, assign) NSInteger columnCount; @@ -37,7 +44,7 @@ There are some properties for you. Although they have default values, I strongly ``` #### Step 2 -Your collectionView's delegate (which often is your view controller) must conforms to `CHTCollectionViewDelegateWaterfallLayout` protocol and implements the required method: +Your collection view's delegate (which often is your view controller) must conforms to `CHTCollectionViewDelegateWaterfallLayout` protocol and implements the required method, all you need to do is return the original size of the item: ``` objc - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; @@ -61,14 +68,14 @@ CHTCollectionViewWaterfallLayout is available under the MIT license. See the LIC Changelog --------- -#### 0.0.6 +#### 0.6 * [Add] Add `minimumColumnSpacing` and `minimumInteritemSpacing` properties. * [Remove] Remove `itemWidth` property. The layout object will calculate a proper item width automatically. * [Change] Rename delegate method `- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout heightForItemAtIndexPath:(NSIndexPath *)indexPath` to `- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath`. It should return original size for each item. * [Fix] Section header and/or footer should work properly. * [Fix] Use `sectionInset` correctly. -#### 0.0.5 +#### 0.5 * [Add] Multiple sections. * [Add] Header and/or footer for section. * [Add] More properties and delegation methods.