iOS 瀑布流实现「建议收藏」

2022-11-17 10:57:19 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

一、先来看看最终的效果吧

二、创建UI

1.首先我们在viewcontroller中创建一个UICollectionView.

代码语言:javascript复制
//主控制器中
#import "ViewController.h"
#import "WaterFallCollectionViewCell.h"
#import "WaterfallFlowLayout.h"

static const NSInteger imageCount = 18;//图片的数量
static NSString * identifier = @"cellID";//重用机制

@interface ViewController ()<UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout>
//定义一个数组用来装图片
@property (nonatomic,strong)NSArray * imageArray;
//定义一个collctionView
@property (nonatomic,strong)UICollectionView * colletionView;
@end

@implementation ViewController
#pragma mark - 懒加载
-(NSArray *)imageArray{
    if (!_imageArray) {
        NSMutableArray * tempArray = [NSMutableArray array];
        for (int i = 0; i < imageCount; i  ) {
            UIImage * image = [UIImage imageNamed:[NSString stringWithFormat:@"image%d",i 1]];
            [tempArray addObject:image];
        }
        _imageArray = tempArray;
    }
    return _imageArray;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    //首先创建一个布局对象,这个对象的是自己写的布局类的对象
    WaterfallFlowLayout * layout = [[WaterfallFlowLayout alloc]init] ;
    self.colletionView = [[UICollectionView alloc]initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
    self.colletionView.backgroundColor = [UIColor orangeColor];
    //两个代理的设置,代理一般就是控制器
    self.colletionView.delegate = self;
    self.colletionView.dataSource = self;
    //注册cell,以便重用,tableview中不用注册,但是collectionview中需要注册
    //这里注册的类应该是自己所使用的cell的类,就是自定义的或者系统提供的
    [self.colletionView registerClass:[WaterFallCollectionViewCell class] forCellWithReuseIdentifier:identifier];
    [self.view addSubview:self.colletionView];
}

我们将collectionview定义为一个属性变量,并在viewDidLoad中对其进行设置:首先我们创建了一个布局对象(layout),类型是我们自己定义的布局类(WaterfallFlowLayout),接着我们又对属性变量collectionview进行了创建,设置了他的frame。然后就是对其代理的设置,collectionview的代理有三个,除了和tableview相同的代理和数据源之外,还有一个布局的代理(UICollectionViewDelegateFlowLayout),这里只设置了两个代理,就是数据源和处理事件的代理。这里需要注意的是tableview的重用机制不需要注册,但是collectionview必须要注册,注册的类是自己定义的cell的类(WaterFallCollectionViewCell),然后再跟上标识。值得一提的是collectionview只能采用重用的方式来加载cell。

2.实现数据源方法

代码语言:javascript复制
#pragma mark - 设置数据源
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return self.imageArray.count;
}

//collectionview的重用和tableview不同,后者可以不用重用,但是前者必须使用重用机制
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    WaterFallCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
    cell.image = [self.imageArray objectAtIndex:indexPath.item];
    return cell;
}

我们这里只有一个段,所以直接返回图片数组的数量就可以了,然后我们又重写了cellForItemAtIndexPath方法,在这个方法里面我们创建了一个自定义的cell (WaterFallCollectionViewCell) ,我们在自定义这个类的时候,给了cell一个属性image,设置好之后,我们就将它返回给代理。

3.设置每一个item的size

代码语言:javascript复制
#pragma mark - item的size
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    UIImage * image = [self.imageArray objectAtIndex:indexPath.item];
    CGSize newSize = [self imageNewSizeWitholdWidth:image.size.width andoldHeight:image.size.height];
    return newSize;
}

#pragma mark - 图片压缩后的新的高度
-(CGSize)imageNewSizeWitholdWidth:(CGFloat)width andoldHeight:(CGFloat)height{
    /*
     图片高度/图片宽度 = 压缩后图片高度/压缩后图片宽度 图片的宽高比不变
     (我们规定压缩后图片宽度是固定的)
     */
    CGFloat newWidth = (self.colletionView.bounds.size.width - kColSpacing*(columnCount   1))/columnCount;
    CGFloat newHeight = height/width * newWidth;
    return CGSizeMake(newWidth, newHeight);
}

由于我们要展示的是图片,所以非常关心图片压缩后的宽高比。第一个方法是用来返回item的size的,它是UICollectionViewDelegateFlowLayout的可选方法,它里面有一个我们自己定义的 imageNewSizeWitholdWidth 的方法,我们用这个方法来对图片按照我们的想法进行压缩,具体公式在代码的注释块中。基本思想就是保持宽高比不变。方法中定义的一些如 kcolspacing,columncount 等常量在后面介绍。

三、创建WaterfallFlowLayout类(创建布局类)

1.设置一些常量

代码语言:javascript复制
#import <UIKit/UIKit.h>
static CGFloat kColSpacing = 10;//列与列之间的间隔
static CGFloat const  columnCount = 3;//列的个数
static UIEdgeInsets edgeInsets = {10,10,10,10};//内边距的集合,依次是上,左,下,右(逆时针)
@interface WaterfallFlowLayout : UICollectionViewFlowLayout

@end

可以从上面的代码中看到,我们自定义的布局类是继承于流式布局类的。

2.属性变量的声明

代码语言:javascript复制
#import "WaterfallFlowLayout.h"

@interface WaterfallFlowLayout()
@property (nonatomic,assign)id<UICollectionViewDelegateFlowLayout> delegate;
@property (nonatomic,strong)NSMutableArray * columnHeightArray;//存放列高度的数组,用于后面比较从最小列开始排
@property (nonatomic,strong)NSMutableDictionary * cellInfoDic;//用于存放cell位置信息的字典
@property (nonatomic,assign)NSInteger  cellCount;//cell的总个数
@end

在属性变量中我们定义了一个 一个delegate,一个用于存放列高度的数组,一个存放cell位置信息的字典,一个cell的总个数。具体的每个变量的作用在后面会介绍。

3.布局

代码语言:javascript复制
#pragma mark - 准备布局
-(void)prepareLayout{
    //准备布局
    [super prepareLayout];
    //初始化列高度的数组
    _columnHeightArray = [NSMutableArray array];
    //初始化cell信息的字典
    _cellInfoDic = [NSMutableDictionary dictionary];
    //设置代理为主控制器,我们的瀑布流是继承于系统的流式布局,而流式布局又继承于collectioinviewlayout,它里面有collectionview
    self.delegate = (id<UICollectionViewDelegateFlowLayout>)self.collectionView.delegate;
    //获取cell的总个数
    _cellCount = [self.collectionView numberOfItemsInSection:0];
    if (_cellCount == 0) {
        return;
    }
    float top = 0;
    for (int i = 0 ; i < columnCount; i  ) {
        //先将里面初始化为0,因为开始的时候item距离最上面的距离是0
        [_columnHeightArray addObject:[NSNumber numberWithFloat:top]];
    }
    //循环调用layoutForItemAtIndexPath方法,为每个cell布局,将indexPath传入,作为布局字典的key
    //layoutAttributesForItemAtIndexPath方法的实现,这里用到了一个布局字典,其实就是将每个cell的位置信息与indexPath相对应,将它们放到字典中,方便后面视图的检索
    for (int i = 0; i < _cellCount; i  ) {
    [self layoutItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
    }
}

我们重写了准备布局的方法,我们在里面设置了这个类的代理是主控制器。如果不设置的话,布局就不能体现出来,就是说最终的界面是显示不出来的,因为没有通过主控制器显示出来。对于高度的数组,我们首先将里面的元素都设置为0,因为在刚开始的时候第一个item距离顶端的距离就是0。然后我们注意到最后调用了一个我们自己写的方法 layoutItemAtIndexPath ,它的实现如下:

代码语言:javascript复制
#pragma mark - 将各个cell的fream等信息放入字典中
-(void)layoutItemAtIndexPath:(NSIndexPath *)indexPath{
   //通过delegate获取item的大小,之前在主控制器中设置过了,其中layout是uicollectionview类的,所以传自身就可以了
    CGSize itemSize = [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
   //然后比较列数组中的列的高度,找出最小高度的列
    float colindex = 0;
    float shorterHeight = [[self.columnHeightArray objectAtIndex:colindex]floatValue];
    for (int i = 0; i < columnCount; i  ) {
        float height = [[self.columnHeightArray objectAtIndex:i]floatValue];
        if (height < shorterHeight) {
            shorterHeight = height;
            //记下此时是第几列
            colindex = i;
        }
    }
    //把上一排最小的列的高度取出来便于后面确定坐标
    float lastMinHeight = [[self.columnHeightArray objectAtIndex:colindex]floatValue];
    //确定当前cell的frame
    CGRect frame = CGRectMake(edgeInsets.left  colindex*(kColSpacing   itemSize.width), edgeInsets.top lastMinHeight, itemSize.width, itemSize.height);
    //更新列高度表中的数据
    [self.columnHeightArray replaceObjectAtIndex:colindex withObject:[NSNumber numberWithFloat: frame.origin.y  itemSize.height]];
    //设置cell信息的字典,每个frame对应一个indexpath
    [self.cellInfoDic setObject:indexPath forKey:NSStringFromCGRect(frame)];
}

在这个方法中,我们首先通过delegate获得了在主控制器中设置过的itemsize。既然要实现实现瀑布流,就需要比较每一列的高度,然后把要插入的item插入到高度最小的那一列去。因此,我们需要比较每一列的高度,找出最小列。找出了高度最小的那一列之后,我们需要设置要插入的item的的位置,所以取出高度最小的那一列的高度,用它来确定item是插入到最小高度那一列的。高度取出来之后,我们就要设置所要插入item的frame了,这里就需要我们之前得到的额itemsize了,我们设想的是,四周都距离10。设置好这一个item的frame之后,我们需要更新列高度数组中的数据,以便于下一次比较还是找出高度最小的列进行插入。最后,我们按照每一个frame对应的indexPath方式将一个cell对应的位置,frame信息存入字典。以便于后面显示的时候直接从字典中取就可以了。

这个方法在 prepareLayout 中进行了循环调用,循环了18次,也就是说每一个cell都需要进行布局,以便字典中存储了每一个cell的布局信息。

4.加载可视范围内的cell

对于通过滑动来获取更多的信息的机制,我们不应该一次性把信息加载完,这样的效率很低,应该在滑动时再去加载应该加载的信息。

要实现这种效果,我们需要重写两个方法: layoutAttributesForElementsInRect 和 layoutAttributesForItemAtIndexPath

代码语言:javascript复制
#pragma mark - 返回可见cell的indexPath
-(NSArray *)indexPathsOfItem:(CGRect)rect{
    //遍历布局字典通过CGRectIntersectsRect方法确定每个cell的rect与传入的rect是否有交集,如果结果为true,则此cell应该显示,将布局字典中对应的indexPath加入数组
    //NSLog(@"indexPathsOfItem");
    NSMutableArray *array = [NSMutableArray array];
    for (NSString *rectStr in _cellInfoDic) {
        CGRect cellRect = CGRectFromString(rectStr);
        if (CGRectIntersectsRect(cellRect, rect)) {
            NSIndexPath *indexPath = _cellInfoDic[rectStr];
            [array addObject:indexPath];
        }
    }
    //返回的这个数组只是屏幕中可以看得见的cell,看不见的那些cell在滑动时会重新计算
    return array;
}
//返回cell的布局信息,如果忽略传入的rect一次性将所有的cell布局信息返回,图片过多时性能会很差
//这个方法返回的一定数量的cell的属性的数组,一定数量指的是出现在屏幕中的cell,不包括滑动出现的cell
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSMutableArray *muArr = [NSMutableArray array];
    //indexPathsOfItem方法,根据传入的frame值计算当前应该显示的cell
    NSArray *indexPaths = [self indexPathsOfItem:rect];
    //NSLog(@"hhh%@",indexPaths);
    for (NSIndexPath *indexPath in indexPaths) {
        //  UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
        UICollectionViewLayoutAttributes *attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
        [muArr addObject:attribute];
    }
    return muArr;
}
//代理方法返回每个cell属性
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    //这个cell的属性
    //NSLog(@"hhhh%@",indexPath);
    UICollectionViewLayoutAttributes * attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    for (NSString * rectStr in self.cellInfoDic) {
        if ([self.cellInfoDic valueForKey:rectStr] == indexPath) {
            //说明是对应的
            attributes.frame = CGRectFromString(rectStr);
        }
    }
    return attributes;
}

第一个方法有一个参数 rect 这个rect的大小是 {0,-736,414,1472}(iphon8plus下),意思就是说这个rect就是两倍的屏幕大,但是屏幕下端的就没有在他那个范围之内。那如何判断cell的rect是否在参数的rect之内呢?我们可以用 CGRectIntersectsRect 这个方法来判断,这个方法的意思是如果两个rect没有交集的话,就返回 no,如果有交集就返回yes。我们写了一个方法 indexPathsOfItem 来将需要显示出来的cell的indexPath放入数组中,以便于在后面取某个cell方便。然后我们在下面的那个方法里面调用这个方法,他返回给我们需要显示的cell对应的indexPath的数组,接着我们通过遍历这个数组取出其中的cell对应的indexPath,然后调用 layoutAttributesForItemAtIndexPath这个方法来得到对应cell的属性(attribute)。这个 layoutAttributesForItemAtIndexPath 方法我们也是重写了的,在这个方法里面既然我们返回对应indexpath的cell的属性 ,就应该判断这个indexpath是不是对应的这个cell,所以我们写了一个判断的方法,获取cell的属性当然还是通过之前的cellInfoDic来获取。

5.设置滑动的contentSize

代码语言:javascript复制
//实现这个方法,跟前面找出最小的列高度的方法如出一辙
-(CGSize)collectionViewContentSize{
    CGSize size = self.collectionView.frame.size;
    float maxHeight = [[self.columnHeightArray objectAtIndex:0]floatValue];
    for (int i = 0; i < columnCount; i  ) {
        float height = [[self.columnHeightArray objectAtIndex:i]floatValue];
        if (height > maxHeight) {
            maxHeight = height;
        }
    }
    size.height = maxHeight   10;
    return size;
}

还是和之前一样,滑动的高度是由最长的那一列的高度决定的,所以我们需要找出最长的那一列。这就和之前找最短一列道理是一样的,这里直接上代码了。

四、自定义cell

1.属性

由于我们的cell是用来展示图片的,所以自然就有image这个属性

代码语言:javascript复制
#import <UIKit/UIKit.h>

@interface WaterFallCollectionViewCell : UICollectionViewCell
@property (nonatomic,strong)UIImage * image;
@end

可以看到,我们自定义的cell是继承于原始的cell的。

2.自定义cell的实现文件

代码语言:javascript复制
#import "WaterFallCollectionViewCell.h"
#import "WaterfallFlowLayout.h"
#define WIDTH ((self.superview.bounds.size.width - kColSpacing*(columnCount   1))/3)
@implementation WaterFallCollectionViewCell

-(void)setImage:(UIImage *)image{
    if (_image != image) {
        _image = image;
    }
}

-(void)drawRect:(CGRect)rect{
    float newHeight = _image.size.height / _image.size.width * WIDTH;
    [_image drawInRect:CGRectMake(0, 0, WIDTH, newHeight)];
    self.backgroundColor = [UIColor grayColor];
}

@end

drawRect 方法由系统调用,drawInRect 方法将图片显示到参数rect中。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/213963.html原文链接:https://javaforall.cn

0 人点赞