前段时间接触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进行设置当达到自己的满意效果即可
豆瓣相册里的布局主要有四种:一、单排左右各一个;二、左一右二;三、左二右一;四、左二右二;代码里已经很好体现了实现过程,希望对需要用到瀑布流的同邪有助
还有就是推荐下载下豆瓣相册的源码看下,希望对你有帮助
最后效果图如下,希望你喜欢