UIColletionView瀑布流佈局實現思路以及封裝的實現

Deft_MKJing宓珂璟發表於2017-01-29

瀑布流實現思路

  • 第一種就是用ScrollView來進行實現,由於它不具備複用的功能,因此我們需要自己寫一套類似複用的模組來進行優化
  • 第二種就是利用apple做好的複用模組,自定義UIColletionLayout來實現瀑布流,想想也是第二種實現起來更快更優,OK,封裝一個小小的框架來試試


    預設兩列


    三列多倍間距

其他案例

UIColletionView瀑布流佈局實現思路以及封裝的實現
上面的動畫切換佈局也是自定義UICollectionLayout來進行佈局的,簡單的靜態圖片佈局展示其實就重寫幾個方法就可以了


1.prepareLayout 每次重新重新整理collectionView的時候會呼叫一次,做一些初始化的工作
2.layoutAttributesForElementsInRect 返回已經制定好之後的每個cell對應的attribute屬性物件進行佈局
3.layoutAttributesForItemAtIndexPath 該方法會一直呼叫,每次cell出來就會根據對應的indexpath來進行方法呼叫,因此關鍵佈局程式碼就可以放置在這裡進行重新計算
4.collectionViewContentSize 計算整體的大小,實現滾動

上面插入樣式實現的傳送門

瀑布流實現分析

1.基本變數的宣告

// 每一列的間距
static const CGFloat MKJDefaultColumnMargin = 10;
// 每一行間距
static const CGFloat MKJDefaultRowMargin = 10;
// 整體的上間距,左間距,下間距,右間距
static const UIEdgeInsets MKJDefaultEdgeInsets = {10,10,10,10};
// 預設是多少列
static const NSUInteger MKJDefaultColumnCounts = 2;

@interface MKJWaterFallLayout ()

@property (nonatomic,strong) NSMutableArray *attributeArr; // cell屬性的陣列
@property (nonatomic,strong) NSMutableArray *columnHeightArr; // 每列的高度陣列

@end

2.初始化

// 每次重新整理會呼叫一次
- (void)prepareLayout
{
    [super prepareLayout];

    // 每次重新重新整理的時候清除之前的所有高度值,預設就是UIEdg給定的top
    [self.columnHeightArr removeAllObjects];
    for (NSInteger i = 0; i < [self columnCount]; i++) {
        [self.columnHeightArr addObject:@([self insetMargin].top)];
    }
    // 每次重新整理把對應的att屬性清空
    [self.attributeArr removeAllObjects];
    // 初始化一次每個cell對應的attribute屬性
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (NSInteger i = 0; i < count; i++) {
        NSIndexPath *indexpath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexpath];
        [self.attributeArr addObject:attribute];
    }
}

3.關鍵計算程式碼

// 返回attribute屬性陣列決定最後的排布
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attributeArr;
}

// 返回對應的indexpath下每個cell的屬性 cell的出現會一直重新整理該方法
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 初始化佈局屬性---> 對應的indexpath
    UICollectionViewLayoutAttributes *att = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGFloat collectionW = self.collectionView.frame.size.width;

    // 寬度是根據列數和間距固定算出來的
    CGFloat width = (collectionW - [self insetMargin].left - [self insetMargin].right - ([self columnCount] - 1) * [self columnMargin]) / [self columnCount];

    // 高度是根據代理的資料來源返回比例計算的
    CGFloat height = [self.delegate MKJWaterFallLayout:self heightForItemAtIndexPath:indexPath] * width;
    // X 和 Y值都是在找出最小column之後才能確定,核心就是根據列數,找出最小高度的那一列
    // 先取出第一個預設是最小的
    CGFloat minColumnHeight = [self.columnHeightArr[0] doubleValue];
    // 預設最小的是第0列
    NSUInteger finalCol = 0;
    for (NSInteger i = 1 ; i < [self columnCount]; i++) {
        CGFloat currentColHeight = [self.columnHeightArr[i] doubleValue];
        if (minColumnHeight > currentColHeight) {
            minColumnHeight = currentColHeight;
            finalCol = i;
        }
    }

    // x,y值是根據最小高度列算出來的
    CGFloat x = [self insetMargin].left + (width + [self columnMargin]) * finalCol;
    CGFloat y = minColumnHeight;
    // 當你是一個行排布的時候 預設是top值,不需要加間距
    NSInteger count = indexPath.item;
    if ((count / ([self columnCount])) >= 1) {
        y += [self rowMargin];
    }
    att.frame = CGRectMake(x, y, width, height);
    self.columnHeightArr[finalCol] = @(CGRectGetMaxY(att.frame));
    return att;

}



這裡的計算簡概括為就是對每個cell進行frame的計算
1.寬度的計算是根據列間距和整體左右間距以及行數進行限制,這些引數可以是固定值,也可以是代理傳進去的
2.高度的計算必定是根據外部代理進行計算的
3.X值的計算是根據這個框架內部的每一列的高度陣列進行之前的快取高度,進行最小值計算,然後拿出最小值對應的列數,根據上面算出來的高度進行X值的計算
4.Y值的計算和X值一樣,根據給定的陣列,比出最小高度列的列數,根據陣列的高度,計算出對應的Y值,最終再進行陣列對應列數的高度更新

4.獲取實際能容大小,讓其可以滾動

// 計算出滾動區域的大小
- (CGSize)collectionViewContentSize
{
    CGFloat maxColumHeight = [self.columnHeightArr[0] doubleValue];
    for (NSInteger i = 1; i < [self columnCount]; i++) {
        CGFloat currentColHeight = [self.columnHeightArr[i] doubleValue];
        if (maxColumHeight < currentColHeight) {
            maxColumHeight = currentColHeight;
        }
    }
    return CGSizeMake(0, maxColumHeight + [self insetMargin].bottom);

}

5.這個小框架已經封裝好了,簡單介紹下用法

// 宣告如下
- (UICollectionView *)colletionView
{
    if (_colletionView == nil) {
        MKJWaterFallLayout *layout = [[MKJWaterFallLayout alloc] init];
        layout.delegate = self;
        UICollectionView *collectionV = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
        collectionV.backgroundColor = [UIColor redColor];
        [collectionV registerNib:[UINib nibWithNibName:productID bundle:nil] forCellWithReuseIdentifier:productID];
        collectionV.delegate = self;
        collectionV.dataSource = self;
        _colletionView = collectionV;

    }
    return _colletionView;
}

// 內部行數,間距的控制
#pragma mark - waterfallLayoutDelegate
- (CGFloat)MKJWaterFallLayout:(MKJWaterFallLayout *)layout heightForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 返回寬度和高度比例
    MKJProductModel *product = self.dataSource[indexPath.item];
    return product.h / product.w;
}

// 控制列間距
- (CGFloat)columnMarginForWaterFallLayout:(MKJWaterFallLayout *)collectionViewLayout
{
    return 10;
}
// 控制行間距
- (CGFloat)rowMarginForWaterFallLayout:(MKJWaterFallLayout *)collectionViewLayout
{
    return 30;
}
// 控制列數
- (NSUInteger)columnCountForWaterFallLayout:(MKJWaterFallLayout *)collectionViewLayout
{
//    if (self.dataSource.count > 50) {
//        return 3;
//    }
    return 3;
}
// 控制整體上左下右間距
- (UIEdgeInsets)insetForWaterFallLayout:(MKJWaterFallLayout *)collectionViewLayout
{
    return UIEdgeInsetsMake(10, 10, 10, 10);
}



一個簡單的Demo以及一個框架就這樣基本設計完了,需要的朋友可以直接下載拖到自己的工程裡面試試,有問題請留言
Demo地址:
Demo和框架傳送門

相關文章