IOS瀑布流 通过自定义UICollectionViewController的Layout布局实现

前段时间接触IOS,在cocoachina代码库的瀑布流一项里面看到一款豆瓣相册应用,写的相当好,于是就拿来复制拷贝山寨了个校友相册

言归正传,豆瓣相册里个人觉得比较有新意的就是这个瀑布流

首先我们可以在StoryBoard里面新建一个UICollectionViewController或者在UIViewController里添加一个UICollectionView,然后对应视图新建相对应得Controller文件设置关联映射,好了,到这第一步就完成了,然后我们继续新建一个类WaterfallLayout继承至UICollectionViewFlowLayout(FlowLayout是UICollectionViewLayout的实例,自定义Layout不支持UICollectionViewLayout,支持UICollectionViewFlowLayout),好了,然后将自定义WaterfallLayout填充到StoryBoard里UICollectionView的Layout属性里,到这第二步就完成了

下图一、二(Layout位置)


图三(布局属性和自定义Layout的大小相关,需要注意一下)


继续往下就进入WaterfallLayout,因为豆瓣相册我没有找到相关的说明文档,就自己瞎猜带想的写了些注释(如果有发现豆瓣相册说明文档的童鞋请告知,叩谢)

下面是Layout代码

//
//
//  Created by Tonny on 12-12-11.
//  Copyright (c) 2012年 SlowsLab. All rights reserved.
//

#import "DAWaterfallLayout.h"
#import "UIView+Additon.h"


@interface DAWaterfallLayout ()//瀑布流布局声明
@property (nonatomic, assign) NSInteger itemCount;//项目数
@property (nonatomic, strong) NSMutableArray *columnHeights; //每一列高度
@property (nonatomic, assign) NSInteger allItems;//总项目数
@property (nonatomic, strong) NSMutableArray *allItemAttributes;
//总项目属性
@end

@implementation DAWaterfallLayout{      //瀑布流布局声明
    CGFloat         nextStyleY;         // 下一个风格的Y值
    
    NSUInteger countForStyle0;     // 风格_0数
    NSUInteger countForStyle1;     // 风格_1数
    NSUInteger countForStyle2;     // 风格_2数
    NSUInteger countForStyle3;     // 风格_3数
    
    NSUInteger style;           //添加新的风格
    
    BOOL        needNewStyle;   //添加新的风格
    
    BOOL        _hasConfigNameAndDescribeLayout;//是否包含配置名描述的布局
}

- (void)clearLayoutAttributes{  //清除布局属性
    _hasConfigNameAndDescribeLayout = NO;   //无包含配置名描述的布局
    needNewStyle = YES;     //需要新的风格
    
    countForStyle0 = 0;     // 风格_0数为0
    countForStyle1 = 0;     // 风格_1数为0
    countForStyle2 = 0;     // 风格_2数为0
    countForStyle3 = 0;     // 风格_3数为0
    style = 0;     //风格数为0
    
    nextStyleY = 65;        // 下一个风格的Y值为65
    _allItems=[[self collectionView] numberOfItemsInSection:0];
    NSLog(@"总项目数为%d",_allItems);
    _allItemAttributes = [NSMutableArray arrayWithCapacity:_allItems];
    //总项目属性(可变数组类型)
    
}

- (UICollectionViewLayoutAttributes *)lastAttributsFrom:(NSArray *)itemsAttributes{
    //    最后一项属性
    if (itemsAttributes.count > 0) {
        //如果项目数大于0则返回最后一个项目属性
        return [itemsAttributes lastObject];
    }else{
        //否则项目数小于等于0返回总项目的最后一个属性
        return [_allItemAttributes lastObject];
    }
}

- (UICollectionViewLayoutAttributes *)lastSecondAttributsFrom:(NSArray *)itemsAttributes{
    //    最后第二项属性
    NSUInteger count = itemsAttributes.count;//项目数
    
    if (count >= 2) {
        //如果项目数大于等于2则返回项目最后第2项
        return [itemsAttributes objectAtIndex:count-2];
    }else if(count == 1){
        //或者项目数为1则返回总项目最后一项
        return [_allItemAttributes lastObject];
    }else{
        //再或者项目数小于等于0则返回总项目最后第2项
        return [_allItemAttributes objectAtIndex:_allItemAttributes.count-2];
    }
}

- (UICollectionViewLayoutAttributes *)lastThirdAttributsFrom:(NSArray *)itemsAttributes{
    //    倒数第三项属性
    NSUInteger count = itemsAttributes.count;//项目数
    if (count >= 3) {
        //如果项目数大于等于3则返回项目最后第3项
        return [itemsAttributes objectAtIndex:count-3];
    }else if(count == 2){
        //或者项目数为2则返回总项目最后一项
        return [_allItemAttributes lastObject];
    }else if(count == 1){
        //或者项目数为1则返回总项目最后2项
        return [_allItemAttributes objectAtIndex:_allItemAttributes.count-2];
    }else{
        //再或者项目数小于等于0则返回总项目最后第3项
        return [_allItemAttributes objectAtIndex:_allItemAttributes.count-3];
    }
}

#pragma mark - Accessors

//- (void)setItemWidth:(CGFloat)itemWidth
//{
//    if (_itemWidth != itemWidth) {
//        _itemWidth = itemWidth;
//        [self invalidateLayout];
//    }
//}

- (id)initWithCoder:(NSCoder *)aDecoder{
    //用于视图件,从storyboard中加载对象实例时,使用initWithCoder初始化这些实例视图对象
    self = [super initWithCoder:aDecoder];
    if (self) {
        nextStyleY = 65;
        
        _allItemAttributes = [[NSMutableArray alloc] initWithCapacity:21];
        //初始化总项目数为21
    }
    
    return self;
}

- (NSUInteger)countOfAlbumTitleAndDescribe{
    //相册的标题和描述统计
    return 1;
}

- (void)configTitleAndDescribeHeaderViewWithCount:(NSUInteger)albumNDCount itemAttributes:(NSMutableArray *)itemAttributes{
    //    配置名称和计数描述标题视图
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
    NSLog(@"indexPath.row=%d",indexPath.row);
    //    当前cell的在tableView中的位置
    UICollectionViewLayoutAttributes *attributes =
    [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    //    UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息
    CGFloat width = [self collectionView].width;
    NSLog(@"collectionView------------------->的width=%f",width);
    if (albumNDCount == 1) {
        //标题描述项为1
        attributes.frame = CGRectMake(0, 0, width-10, 60);
        //        标题栏宽度为310,高为60
        NSLog(@"attributes.frame : %@", NSStringFromCGRect(attributes.frame));
    }else{
        //标题描述项为2
        attributes.frame = CGRectMake(10, 0, width*90/300, 60);
        //        标题栏宽度为96,高为60
        NSLog(@"attributes.frame: %@", NSStringFromCGRect(attributes.frame));
    }
    
    [itemAttributes addObject:attributes];
    if (albumNDCount == 2) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:1 inSection:0];
        UICollectionViewLayoutAttributes *attributes =
        [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        
        CGFloat x = width*90/300+5;
        attributes.frame = CGRectMake(x, 0, width-10-x, 60);
        //        标题栏宽度为209,高为60
        [itemAttributes addObject:attributes];
    }
}

#pragma mark - Methods to Override

//TODO 糟糕的代码
- (void)prepareLayout
{
    [super prepareLayout];
    
    UICollectionView *collectionView = [self collectionView];
    NSUInteger totalItemCount = [collectionView numberOfItemsInSection:0];//项目数
    NSLog(@"totalItemCount=%d",totalItemCount);
    if (totalItemCount == 0) return;
    
    UIInterfaceOrientation oritation = [[collectionView viewController] interfaceOrientation];
    
    NSUInteger itemCount = 0;//内容项数
    NSUInteger titleDesItemCount = 0;//标题项数
    if (!_hasConfigNameAndDescribeLayout) { //first time
        NSLog(@"first time");
        titleDesItemCount = [self countOfAlbumTitleAndDescribe];
        if (titleDesItemCount == totalItemCount) return;
        
        _hasConfigNameAndDescribeLayout = YES;
        
        itemCount = totalItemCount-titleDesItemCount;
        //        内容项数=总数-标题数
        [self configTitleAndDescribeHeaderViewWithCount:titleDesItemCount itemAttributes:_allItemAttributes];
        NSLog(@"titleDesItemCount=%d",titleDesItemCount);
        if (itemCount <= 2) {
            //根据项目数设置风格数
            //          如果项目少于等于2个风格数为0
            style = 0;
        }else if(itemCount == 3){
            //          如果项目为3风格数为1或2
            style = rand()%2+1; //1, 2
        }else if(itemCount == 4){
            //          如果项目为4风格数为3
            style = 3;
        }else{
            //            如果项目大于4风格数为0,1,2,3
            NSUInteger min=MIN(itemCount, 4);
            style = rand()%(min); //0, 1, 2, 3
        }
        NSLog(@"style: %d", style);
    }else{
        //        加载更多
        itemCount = [collectionView numberOfItemsInSection:0]-_allItemAttributes.count;
        
        if (itemCount == 0) return;
    }
    
    CGFloat contentWidth = collectionView.width-2*5;
    NSLog(@"contentWidth: %f", contentWidth);
    NSUInteger X = 0;
    //    x轴启始位置
    NSUInteger Y = 0;
    //    y轴启始位置
    NSUInteger min = 100;
    //    最短高度
    CGFloat width = 0;
    //    宽度
    CGFloat height = 0;
    //    高度
    NSMutableArray *itemAttributes = [NSMutableArray arrayWithCapacity:18];
    //    添加属性数组
    NSUInteger start = _allItemAttributes.count;
    //    瀑布流添加开始位置
    NSLog(@"start: %d", start);
    for (NSInteger idx = start; idx < itemCount+start; idx++) {
        //        主方法
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:0];
        //        每一项位置
        UICollectionViewLayoutAttributes *attributes =
        [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        //        属性
        if (style == 0) {
            //            风格数为0(两种风格:左一右一)
            if (countForStyle0 == 0) {
                //                风格0位置0的设置
                X = 0;
                Y = nextStyleY;
                
                if (UIInterfaceOrientationIsPortrait(oritation)) {
                    width = arc4random()%(150-100)+125;
                    //                    竖屏宽度为(125-174)
                }else{
                    width = arc4random()%(295-200)+200;
                    //                    横屏宽度为(200-294)
                }
                NSLog(@"风格0位置0width_: %f", width);
                height = 100;
                //                高度为100
                countForStyle0++;
                
                needNewStyle = NO;
            }else{
                //               风格0位置1的设置
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                CGRect lastFrame0 = attributes.frame;
                //                风格0位置0的布局属性
                X = lastFrame0.origin.x+lastFrame0.size.width+5.0;
                //                size表示矩形的大小(CGSize)
                NSLog(@"风格0位置1X: %d", X);
                Y = lastFrame0.origin.y;
                NSLog(@"风格0位置1Y: %d", Y);
                width = contentWidth-lastFrame0.size.width-5.0;
                NSLog(@"风格0位置1width: %f", width);
                height = lastFrame0.size.height;
                NSLog(@"风格0位置1height: %f", height);
                nextStyleY = Y+height+5.0;
                NSLog(@"风格0位置1的nextStyleY: %f", nextStyleY);
                countForStyle0 = 0;
                
                needNewStyle = YES;
            }
        }else if(style == 1){
            //            风格数为1(三种风格:左大一右小二)
            if (countForStyle1 == 0) {
                //                风格1位置0的设置
                X = 0;
                Y = nextStyleY;
                
                if (UIInterfaceOrientationIsPortrait(oritation)) {
                    width = arc4random()%(150-100)+125;
                    //                    竖屏宽度为(125-174)
                }else{
                    width = arc4random()%(295-200)+200;
                    //                    横屏宽度为(200-294)
                }
                NSLog(@"风格1位置0width_: %f", width);
                height = arc4random()%(260-255)+255;
                //                    高度为(255-259)
                NSLog(@"风格1位置0height_: %f", height);
                countForStyle1++;
                needNewStyle = NO;
            }else if (countForStyle1 == 1) {
                //                风格1位置1的设置
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                CGRect lastFrame0 = attributes.frame;
                //                风格1位置0的布局属性
                X = lastFrame0.origin.x+lastFrame0.size.width+5.0;
                //                size表示矩形的大小(CGSize)
                NSLog(@"风格1位置1X: %d", X);
                Y = nextStyleY;
                NSLog(@"风格1位置1Y: %d", Y);
                width = contentWidth-lastFrame0.size.width-5.0;
                NSLog(@"风格1位置1width: %f", width);
                height = arc4random()%(120-min)+min;
                //                高度为(100-119)
                NSLog(@"风格1位置1height: %f", height);
                nextStyleY = Y+height+5.0;
                NSLog(@"风格1位置1的nextStyleY: %f", nextStyleY);
                countForStyle1++;
                needNewStyle = NO;
            }else{
                //                风格1其他位置的设置
                UICollectionViewLayoutAttributes *attributes1 = [self lastSecondAttributsFrom:itemAttributes];
                //                风格1位置0的布局属性
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                //                风格1位置1的布局属性
                CGRect lastFrame0 = attributes.frame;
                //                风格1位置1的位置和大小
                X = lastFrame0.origin.x;
                //                origin表示矩形左上角所在位置(CGPoint)
                NSLog(@"风格1位置2X: %d", X);
                Y = lastFrame0.origin.y+lastFrame0.size.height+5.0;
                NSLog(@"风格1位置2Y: %d", Y);
                width = lastFrame0.size.width;
                NSLog(@"风格1位置2width: %f", width);
                CGRect lastFrame1 = attributes1.frame;
                //                风格1位置0的位置和大小
                height = lastFrame1.size.height-lastFrame0.size.height-5.0;
                NSLog(@"风格1位置2height: %f", height);
                nextStyleY = Y+height+5.0;
                NSLog(@"风格1位置2的nextStyleY: %f", nextStyleY);
                countForStyle1 = 0;
                needNewStyle = YES;
            }
        }else if(style == 2){
            //              风格数为2(三种风格:左小二右大一)
            if (countForStyle2 == 0) {
                X = 0;
                Y = nextStyleY;
                
                if (UIInterfaceOrientationIsPortrait(oritation)) {
                    width = arc4random()%(150-100)+125;
                    //                    竖屏宽度为(125-174)
                }else{
                    width = arc4random()%(295-200)+200;
                    //                    横屏宽度为(200-294)
                }
                
                height = arc4random()%(120-min)+min;
                //                高度为(100-119)
                countForStyle2++;
                needNewStyle = NO;
            }else if (countForStyle2 == 1) {
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                CGRect lastFrame0 = attributes.frame;
                
                //                X = lastFrame0.origin.x;
                //                Y = lastFrame0.origin.y+lastFrame0.size.height+5.0;
                //                width = lastFrame0.size.width;
                //                与前一个布局等宽
                //                height = arc4random()%(150-min)+min;
                //                高度为(100-150)
                X = lastFrame0.origin.x+lastFrame0.size.width+5.0;
                Y = lastFrame0.origin.y;
                width = contentWidth-lastFrame0.size.width-5.0;
                height = arc4random()%(260-255)+255;
                //                    高度为(255-259)
                
                countForStyle2++;
                needNewStyle = NO;
            }else if (countForStyle2 == 2) {
                UICollectionViewLayoutAttributes *attributes1 = [self lastSecondAttributsFrom:itemAttributes];
                //                风格2位置0的布局属性
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                //                风格2位置1的布局属性
                CGRect frame1 = attributes1.frame;
                //                风格2位置0的位置和大小
                CGRect frame0 = attributes.frame;
                //                风格2位置1的位置和大小
                X = frame1.origin.x;
                Y = frame1.origin.y+frame1.size.height+5.0;
                
                width = frame1.size.width;
                
                height = frame0.size.height-frame1.size.height-5.0;
                //                高为前两布局之和
                nextStyleY = Y+height+5.0;
                countForStyle2 = 0;
                needNewStyle = YES;
            }
        }else if(style == 3){
            //            风格数为3(四种风格:左二右二)
            if (countForStyle3 == 0) {
                X = 0;
                Y = nextStyleY;
                
                if (UIInterfaceOrientationIsPortrait(oritation)) {
                    width = arc4random()%(150-100)+125;
                    //                    竖屏宽度为(145-174)
                }else{
                    width = arc4random()%(295-200)+200;
                    //                     横屏宽度为(200-294)
                }
                
                height = arc4random()%(120-min)+min;
                //                高度为(100-119)
                countForStyle3++;
                needNewStyle = NO;
            }else if (countForStyle3 == 1) {
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                CGRect lastFrame0 = attributes.frame;
                
                X = lastFrame0.origin.x+lastFrame0.size.width+5.0;
                
                Y = lastFrame0.origin.y;
                
                width = contentWidth-lastFrame0.size.width-5.0;
                //                与前一个布局等宽
                height = arc4random()%(120-min)+min+20;
                //                高度为(120-139)
                countForStyle3++;
                needNewStyle = NO;
            }else if (countForStyle3 == 2) {
                UICollectionViewLayoutAttributes *attributes1 = [self lastSecondAttributsFrom:itemAttributes];
                //                风格3位置0的布局属性
                CGRect lastFrame1 = attributes1.frame;
                //                风格3位置0的位置和大小
                X = lastFrame1.origin.x;
                
                Y = lastFrame1.origin.y+lastFrame1.size.height+5.0;
                
                width = lastFrame1.size.width;
                
                height = arc4random()%(120-min)+min+20;
                //                高度为(120-139)
                nextStyleY = Y+height+5.0;
                countForStyle3++;
                needNewStyle = NO;
            }else{
                UICollectionViewLayoutAttributes *attributes2 = [self lastThirdAttributsFrom:itemAttributes];
                //                风格3位置0的布局属性
                UICollectionViewLayoutAttributes *attributes1 = [self lastSecondAttributsFrom:itemAttributes];
                //                风格3位置1的布局属性
                UICollectionViewLayoutAttributes *attributes = [self lastAttributsFrom:itemAttributes];
                //                风格3位置2的布局属性
                CGRect lastFrame2 = attributes2.frame;
                //                风格3位置0的位置和大小
                CGRect lastFrame1 = attributes1.frame;
                //                风格3位置1的位置和大小
                CGRect lastFrame0 = attributes.frame;
                //                风格3位置2的位置和大小
                X = lastFrame1.origin.x;
                Y = lastFrame1.origin.y+lastFrame1.size.height+5.0;
                
                width = lastFrame1.size.width;
                
                height = lastFrame2.size.height+lastFrame0.size.height-lastFrame1.size.height;
                
                nextStyleY = Y+height+5.0;
                countForStyle3 = 0;
                needNewStyle = YES;
            }
        }
        
        //        SLLog(@"style %d (%d %d %d %d), (%d %d %.1f %.1f)", style, countForStyle0, countForStyle1, countForStyle2, countForStyle3, X, Y, width, height);
        attributes.frame = CGRectMake(X, Y, width, height);
        
        if (Y+height >= _contentSizeHeight) {
            _contentSizeHeight = Y+height;
        }
        
        if (needNewStyle) {
            NSUInteger remainder = itemCount-(idx-start);
            if (remainder <= 2) {
                style = 0;
            }else if(remainder == 3){
                style = rand()%2+1; //1, 2
            }else if(remainder == 4){
                style = 3;
            }else{
                NSUInteger originStyle = style;
                style = rand()%(MIN(remainder, 4));
                
                if (originStyle == style) {
                    if (style >= 1) {
                        style--;
                    }else{
                        style = 3;
                    }
                }
            }
        }
        
        [itemAttributes addObject:attributes];
    }
    
    [_allItemAttributes addObjectsFromArray:itemAttributes];
}

- (CGSize)collectionViewContentSize
{
    //    返回collectionView的内容的尺寸
    CGSize contentSize = self.collectionView.frame.size;
    contentSize.height = _contentSizeHeight+5.0+20+5.0;
    NSLog(@"contentSize: %@", NSStringFromCGSize(contentSize));
    return contentSize;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
    //    Supplementary Views(补充的view,相当于TableView的页眉和页脚)
    //    返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
    UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
    
    CGRect frame = attributes.frame;
    frame.origin.y = _contentSizeHeight+5.0;
    //    origin就是所謂的起點位置
    attributes.frame = frame;
    
    return attributes;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    //    为每个index path创建并配置一个合适的布局属性对象,并将每个对象添加到数组中
    //    返回对应于indexPath的位置的cell的布局属性
    return _allItemAttributes[path.item];
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    //    这是任何布局类中最重要的方法了,同时可能也是最容易让人迷惑的方法。collection view调用这个方法并传递一个自身坐标系统中的矩形过去。这个矩形代表了这个视图的可见矩形区域(也就是它的bounds),你需要准备好处理传给你的任何矩形
    //    返回rect中的所有的元素的布局属性
    //    返回的是包含UICollectionViewLayoutAttributes的NSArray
    NSMutableArray *muArr = [NSMutableArray arrayWithArray:_allItemAttributes];
    [muArr addObject:[self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]];
    
    return muArr;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    //    当collection view的bounds改变时,布局需要告诉collection view是否需要重新计算布局
    return NO;
}

到这就基本上完成这个瀑布流的实现了

说下Layout的实现过程,主要通过覆写prepareLayout方法实现,首要获得UICollectionViewLayoutAttributes实例,对LayoutAttributes进行设置当达到自己的满意效果即可

豆瓣相册里的布局主要有四种:一、单排左右各一个;二、左一右二;三、左二右一;四、左二右二;代码里已经很好体现了实现过程,希望对需要用到瀑布流的同邪有助

还有就是推荐下载下豆瓣相册的源码看下,希望对你有帮助

最后效果图如下,希望你喜欢








评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值