UICollectionView的靈活布局 --從一個需求談起

最近想把自己搞的一些東西寫出來,剛把之前文章的排版整理了整理,以后一定要堅持更新簡書了。

那么這篇文章來了,一切的起源在于一個這樣的布局需求。


Simulator Screen Shot 2015年11月27日 下午11.13.27.png

首先就想到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

寫文不易,大家看到了順手點個喜歡唄~ 也讓我更有動力分享些東西,謝謝啦~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容