瀑布流可以在保证图片原始比例的情况下,灵活的展现内容,相对于传统的使用相同大小的网格展现大量图片,要好上很多,而实现瀑布流的方式有很多种,网上比较流行的有三种实现方式。
1,使用UIScrollView,主要技术点在于视图的重用。
2,使用UITableView,这种方式应该是最易想到的,因为需要展现几列就用几个tabelview就ok了,而且不需要考虑重用,应为苹果已经做好了,只需要考虑如何在几列tabelView滑动的时候,保持同步不出现BUG。
3,使用UICollectionView,UICollectionView在iOS6中第一次被介绍,它与UITableView有许多相似点,但它多了一个布局类,而实现瀑布流,就与这个布局类有关。此种方式实现,也不需要考虑视图重用。
以上三种方式实现瀑布流,使用UICollectionView应该算是最简单的了,so,就重最简单的开始吧。
由于网络太差,所以展现的并不是网络上的图片,而是将用户相册中的图片读取出,并用瀑布流展现。
首先,遍历用户相册,将照片放到准备好的数组中。访问用户相册需要导入<AssetsLibrary/AssetsLibrary.h>框架
<span style="font-size:14px;"> _images = [NSMutableArray array];
//创建相册库
library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
ALAssetRepresentation *resentation = [result defaultRepresentation];
CGImageRef imageRef = resentation.fullResolutionImage;
UIImage *image = [UIImage imageWithCGImage:imageRef];
//将相片加入到数组中
[self.images addObject:image];
[_collectionView reloadData];
}
}];
}
} failureBlock:^(NSError *error) {
}];
</span>
然后创建UICollectionView,注意这里放置UICollectionView的控制器要实现,UICollectionViewDelegateFlowLayout和UICollectionViewDatasource两个协议。
UICollectionView创建时需要传入一个布局对象,布局类继承自UICollectionViewLayout这个抽象基类,我们需要自定义布局对象,继承
UICollectionViewLayout的子类,UICollectionViewFlowLayout类,命名为WaterFlowLayout。
<span style="font-size:14px;">@interface WaterFlowLayout : UICollectionViewFlowLayout
@property (nonatomic,assign)id<UICollectionViewDelegateFlowLayout> delegate;
@end
</span>
<span style="font-size:14px;"> WaterFlowLayout *layout = [[WaterFlowLayout alloc] init];
_collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
[self.view addSubview:_collectionView];
//注册单元格
[_collectionView registerClass:[WaterfallCell class] forCellWithReuseIdentifier:indf];
</span>
设置好代理,下面实现代理协议就行了,在这里同样子类化了UICollectionViewCell,命名为WaterfallCell,并设置一个image属性,便于在协议方法中将照片传入。
<span style="font-size:14px;">@interface WaterfallCell : UICollectionViewCell
@property (nonatomic,retain)UIImage *image;
@end</span>
实现UICollectionViewDatasource协议。
<span style="font-size:14px;">- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return _images.count;
}
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
WaterfallCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:indf forIndexPath:indexPath];
UIImage *image = _images[indexPath.item];
cell.image = image;
return cell;
}
</span>
实现UICollectionViewDelegateFlowLayout协议,将单元格的尺寸传入,这里让每张图片的宽度保持相同,高度等比例缩放。
<span style="font-size:14px;">- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
UIImage *image = _images[indexPath.item];
float height = [self imgHeight:image.size.height Width:image.size.width];
return CGSizeMake(100,height);
}
//单元格中的间隙
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{
UIEdgeInsets edgeInsets = {5,5,5,5};
return edgeInsets;
}
- (float)imgHeight:(float)height Width:(float)width{
//单元格固定宽度为100 高度/宽度 = 压缩后高/100
float newHeigth = height/width*100;
return newHeigth;
}
</span>
以上完成以后,控制器中的逻辑基本处理完毕,下面重写cell的setter方法,简单起见,我并没有创建Imageview显示图片,而是将图片直接绘制在了单元格上。
- (void)setImage:(UIImage *)image{
if (_image != image) {
_image = image;
}
[self setNeedsDisplay];
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
float newHeigth = _image.size.height/_image.size.width*100;
[_image drawInRect:CGRectMake(0, 0, 100, newHeigth)];
}
cell处理完毕后,下面就是最重要的布局类了,瀑布流就是依靠此类来展现的
首先在- (void)prepareLayout方法中得到cell的总个数,为每个cell确定自己的位置,此处用三列展现图片,当然列数可以随意设置,这取决与图片的宽度。
在layoutForItemAtIndexPath方法中为每个cell做布局,首先创建一个数组,存储三列的高度,每次遍历数组,找出最短列,将cell插入到最短列,而cell的高度和宽度,可以通过调用前面视图控制器实现的代理协议得到。
//准备布局
- (void)prepareLayout{
[super prepareLayout];
attributesDic = [NSMutableDictionary dictionary];
self.delegate = (id<UICollectionViewDelegateFlowLayout>)self.collectionView.delegate;
//获取cell的总个数
cellCount = [self.collectionView numberOfItemsInSection:0];
if (cellCount == 0) {
return;
}
colArr = [NSMutableArray array];//列数 此处为3列 数组中存储每列的高度
float top = 0;
for (int i = 0; i < 3; i++) {
[colArr addObject:[NSNumber numberWithFloat:top]];
}
//循环调用layoutAttributesForItemAtIndexPath方法为每个cell计算布局,将indexPath传入,做为布局字典的key
for (int i =0; i < cellCount; i++) {
[self layoutForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
}
}
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">下面是</span><span style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; background-color: rgb(255, 255, 255);">layoutForItemAtIndexPath方法的实现,这里用到了一个布局字典,其实就是将每个cell的位置信息与indexPath相对应,将它们存入字典中,之所以用到这个字典,是为了后面视图的检索。</span>
//此方法会多次调用 为每个cell布局
- (void)layoutForItemAtIndexPath:(NSIndexPath *)indexPath{
//调用协议方法得到cell间的间隙
UIEdgeInsets edgeInsets = [self.delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:indexPath.row];;
edge = edgeInsets;
CGSize itemSize = [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
NSInteger col = 0;//列数
//找出高度最小列,将cell加到最小列中
float shortHeigth = [[colArr objectAtIndex:col] floatValue];
for (int i = 1; i< colArr.count; i++) {
float colHeight = [[colArr objectAtIndex:i] floatValue];
if (colHeight < shortHeigth) {
shortHeigth = colHeight;
col = i;
}
}
float top = [[colArr objectAtIndex:col] floatValue];
//确定每个cell的frame
CGRect frame = CGRectMake(edgeInsets.left + col*(edgeInsets.left+itemSize.width), top+edgeInsets.top, itemSize.width, itemSize.height);
//cell加入后,更新列高
[colArr replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:top + edgeInsets.top+itemSize.height]];
//每个cell的frame对应一个indexPath,存入字典中
[attributesDic setObject:indexPath forKey:NSStringFromCGRect(frame)];
}
为每个cell布局完毕后,还需要实现一个很重要的方法,
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
此方法会传入一个UICollectionView当前可见的frame,需要返回需要显示的cell信息,数组中需要装入
UICollectionViewLayoutAttributes类创建的对象。
UICollectionViewLayoutAttributes类包含了collection view内item的所有相关布局属性。默认情况下,这个类包含frame,center,size,transform3D,alpha等信息,其实就是对每个cell位置信息的抽象。
当然你可以忽略传入的rect简单粗暴的将所有cell的布局信息放入数组中全部返回,我看了网上很多代码也都是这样做的,这样做是可以实现效果了,按cocoa上的一篇关于UICollectionView译文的说法,此种做法是一种很幼稚的实现。因为当可见的cell远少于不可见cell的数量时,性能可能会灰常差,ok,为了使实现不那么幼稚,前面的布局字典就用到了。
遍历布局字典,将每个cell的frame与系统传入的frame使用CGRectIntersectsRect方法,判断字典中的那些cell的frame与传入的frame有交集如果结果为true说明此cell需要显示,那么就将cell对应的indexPath放入准备好的数组中,之所以需要indexPath,是因为UICollectionViewLayoutAttributes类需要indexPath来实例化,以此来对应每个cell。
- (NSArray *)indexPathsOfItemsInRect:(CGRect)rect{
//遍历布局字典通过CGRectIntersectsRect方法确定每个cell的rect与传入的rect是否有交集,如果结果为true,则此cell应该显示,将布局字典中对应的indexPath加入数组
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSString *rectStr in attributesDic) {
CGRect cellRect = CGRectFromString(rectStr);
if (CGRectIntersectsRect(rect, cellRect)) {
NSIndexPath *indexPath = attributesDic[rectStr];
[indexPaths addObject:indexPath];
}
}
return indexPaths;
/此方法会传入一个collectionView当前可见的rect,视图滑动时调用
//需要返回每个cell的布局信息,如果忽略传入的rect一次性将所有的cell布局信息返回,图片过多时性能会很差
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *attributes = [NSMutableArray array];
//调用indexPathsOfItemsInRect方法,将rect传入,计算当前rect中应该出现的cell,返回值为cell的indexPath数组
NSArray *indexPaths = [self indexPathsOfItemsInRect:rect];
for (NSIndexPath *indexPath in indexPaths) {
UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
[attributes addObject:attribute];
}
return attributes;
}
最后还需要实现
- (CGSize)collectionViewContentSize方法,此方法需要返回,collectionView内容的大小,只需要遍历前面创建的存放列高的数组得到列高最大的一个作为高度返回就可以了
//此方法返回collectionView内容的高度
- (CGSize)collectionViewContentSize{
CGSize size = self.collectionView.frame.size;
//找出3列中最高的一列,作为collectionView的高度
float maxHeight = [[colArr objectAtIndex:0] floatValue];
for (int i = 1; i < colArr.count; i++) {
float colHeight = [[colArr objectAtIndex:i] floatValue];
if (colHeight > maxHeight) {
maxHeight = colHeight;
}
}
size.height = maxHeight;
return size;
}
至此一个简单的瀑布流已经实现完毕,下面看一下效果图