最近想把自己搞的一些東西寫出來,剛把之前文章的排版整理了整理,以后一定要堅持更新簡書了。
那么這篇文章來了,一切的起源在于一個這樣的布局需求。
首先就想到collectionView。用tableView也能強行實現就是了,但是比較笨重,改動布局就得重畫cell,所以本文就詳細介紹下我怎么實現這個需求的。
此處先放點新手福利
如果你沒接觸過UICollectionView,但對UITableView比較熟悉的,可以看下這段,熟悉UICollectionView可以直接跳過。連UITableView都不熟的建議從基礎開始學。
繪制UICollectionView類似于UITableView,滿足delegate和dataSouce兩個代理。
注意的區別是:
1、UICollectionView的indexPath,通常使用item的屬性,印象中row的屬性跟UITableView不一樣,和section毫無關系。
2、每個section的head和footViewz在這個代理里實現。
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
這個view可以像cell一樣復用的。在初始化時注冊:
[self.collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionElementKindSectionHeader"];
使用時:
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionReusableView *reusableview = nil;
if (kind == UICollectionElementKindSectionHeader) {
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionElementKindSectionHeader" forIndexPath:indexPath];
headerView.backgroundColor = [UIColor redColor];
reusableview = headerView;
}
return reusableview;
3、UICollectionView的cell選中的時候是有backgroundView和selectedBackgroundView兩個屬性的,可以做出一些選擇效果:
@property (nonatomic, strong, nullable) UIView *backgroundView;
@property (nonatomic, strong, nullable) UIView *selectedBackgroundView;
4、每個UICollectionView初始化都要有個UICollectionViewLayout來實現布局,用系統自帶的布局UICollectionViewDelegateFlowLayout的話,滿足這個代理設置高度:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
UICollectionViewDelegateFlowLayout里還有好幾個參數和其他的代理,有興趣的同學可以去看看api,然后寫個demo測試一下,因為蠻簡單的,這里就不再深究了。。
新手福利結束
回歸正題
現在回到我我們的需求上。
滿足這個需求我們必須重寫UICollectionViewLayout。
核心重寫的方法如下:
//每次布局都會調用
- (void)prepareLayout;
//布局完成后設置contentSize
- (CGSize)collectionViewContentSize;
//返回每個item的屬性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
//返回所有item屬性
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
當然最重要的是,你的一些參數得通過代理或者Block傳出來賦值。
/*
* 獲取item寬高
*
* @param block 返回寬高的block
*/
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block;
下面是代碼,注釋豐富,可以放心閱讀:
HomeCollectionLayout.h:
typedef CGSize(^SizeBlock)(NSIndexPath *indexPath);
@interface HomeCollectionLayout : UICollectionViewLayout
/** 行間距 */
@property (nonatomic, assign) CGFloat rowSpacing;
/** 列間距 */
@property (nonatomic, assign) CGFloat lineSpacing;
/** 內邊距 */
@property (nonatomic, assign) UIEdgeInsets sectionInset;
/*
* 獲取item寬高
*
* @param block 返回寬高的block
*/
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block;
HomeCollectionLayout.m
@interface HomeCollectionLayout()
/** 計算每個item高度的block,必須實現*/
@property (nonatomic, copy) SizeBlock block;
/** 存放元素高寬的鍵值對 */
@property (nonatomic, strong) NSMutableArray *arrOfSize;
/**存放所有item的attrubutes屬性 */
@property (nonatomic, strong) NSMutableArray *array;
/**存放所有section的高度的 */
@property (nonatomic, strong) NSMutableArray *arrOfSectionHeight;
/**總section高度,用于直接輸出contentSize */
@property (nonatomic,assign) CGFloat collectionSizeHeight;
/**總共item個數 */
@property (nonatomic,assign) NSInteger itemCount;
@property (nonatomic,assign) CGFloat collectionWidth;
@end
@implementation HomeCollectionLayout
- (instancetype)init
{
self = [super init];
if (self) {
//對默認屬性進行設置
_arrOfSize = [NSMutableArray array];
_array = [NSMutableArray array];
_arrOfSectionHeight = [NSMutableArray array];
self.itemCount = 0;
self.collectionSizeHeight = 0;
self.sectionInset = UIEdgeInsetsMake(2, 0, 0, 0);
self.lineSpacing = 1;
self.rowSpacing = 1;
}
return self;
}
/**
* 準備好布局時調用
*/
- (void)prepareLayout {
[super prepareLayout];
//reload的時候清空原有數據
[_array removeAllObjects];
[_arrOfSize removeAllObjects];
[_arrOfSectionHeight removeAllObjects];
_collectionSizeHeight = 0;
_itemCount = 0;
NSInteger sectionCount = [self.collectionView numberOfSections];
//根據每個indexPath儲存
for (NSInteger i = 0 ; i < sectionCount; i++) {
NSInteger rowCount = [self.collectionView numberOfItemsInSection:i];
//存儲item的總數目
self.itemCount += rowCount;
//存儲每個列數的長度
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
//計算該section列數
NSInteger lines = 0;
CGSize size = CGSizeZero;
if (self.block != nil) {
size = self.block([NSIndexPath indexPathForRow:0 inSection:i]);
}else{
NSAssert(size.width != 0 ,@"未實現block");
}
lines = self.collectionWidth/size.width;
//存儲每個列數的長度
for (NSInteger k = 0; k < lines; k++) {
[dict setObject:@(self.sectionInset.top) forKey:[NSString stringWithFormat:@"%ld",(long)k]];
}
[_arrOfSize addObject:dict];
for (NSInteger j = 0; j < rowCount; j++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
//調用item計算。
[_array addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
//此時dict已經改變
NSMutableDictionary *mdict = _arrOfSize[i];
//計算每個section的高度
__block NSString *maxHeightline = @"0";
[mdict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) {
if ([mdict[maxHeightline] floatValue] < [obj floatValue] ) {
maxHeightline = key;
}
}];
[self.arrOfSectionHeight addObject:mdict[maxHeightline]];
self.collectionSizeHeight += [mdict[maxHeightline] floatValue];
NSLog(@"\ncontentSize = %@ height = %f\n\n",NSStringFromCGSize(CGSizeMake(self.collectionView.bounds.size.width, self.collectionSizeHeight)),[mdict[maxHeightline] floatValue]);
}
}
/**
* 設置可滾動區域范圍
*/
- (CGSize)collectionViewContentSize {
return CGSizeMake(self.collectionView.bounds.size.width, self.collectionSizeHeight);
}
/**
* 計算indexPath下item的屬性的方法
*
* @return item的屬性
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
//創建item的屬性
UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGSize size = CGSizeZero;
if (self.block != nil) {
size = self.block(indexPath);
}else{
NSAssert(size.width != 0 ,@"未實現block");
}
CGRect frame;
frame.size = size;
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[_arrOfSize objectAtIndex:indexPath.section]];
//循環遍歷找出高度最短行
__block NSString *lineMinHeight = @"0";
[dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) {
if ([dict[lineMinHeight] floatValue] > [obj floatValue]) {
lineMinHeight = key;
}
}];
int line = [lineMinHeight intValue];
//找出最短行后,計算item位置
frame.origin = CGPointMake(line * (size.width + self.lineSpacing), [dict[lineMinHeight] floatValue] + self.collectionSizeHeight);
dict[lineMinHeight] = @(frame.size.height + self.rowSpacing + [dict[lineMinHeight] floatValue]);
//存儲高度
[_arrOfSize replaceObjectAtIndex:indexPath.section withObject:dict];
attr.frame = frame;
NSLog(@"\nframe = %@,indexPath = %@\n\n",NSStringFromCGRect(frame),indexPath);
return attr;
}
/**
* 返回視圖框內item的屬性,可以直接返回所有item屬性
*/
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
return _array;
}
#pragma mark - data source
/**
* 設置計算高度block方法
*
* @param block 計算item高度的block
*/
- (void)calculateItemSizeWithWidthBlock:(CGSize (^)(NSIndexPath *indexPath))block {
if (self.block != block) {
self.block = block;
}
}
#pragma mark - getter & setter
- (CGFloat)collectionWidth {
return self.collectionView.frame.size.width;
}
@end
demo地址:本文demo
小結:跟一般瀑布流不同,這種布局collectionItem的size全部要自己定制,比起強行畫來說,這么做以后更好改。就是算死我了,算法還是需要加強。
另外,這里另外一名作者Tuberose寫了篇更詳細的關于瀑布流的文章:
想更深入研究的同學可以移步這里:瀑布流小框架
簡書已經棄用,歡迎移步我的小專欄:
https://xiaozhuanlan.com/dahuihuiiOS
寫文不易,大家看到了順手點個喜歡唄~ 也讓我更有動力分享些東西,謝謝啦~