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

iOS 瀑布流实现「建议收藏」一、先来看看最终的效果吧二、创建UI   1.首先我们在viewcontroller中创建一个UICollectionView.//主控制器中#import"ViewController.h"#import"WaterFallCollectionViewCell.h"#import"WaterfallFlowLayout.h"staticconstNSIn…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

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

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

二、创建UI

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

//主控制器中
#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];
}

Jetbrains全家桶1年46,售后保障稳定

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

     2.实现数据源方法

#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

#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.设置一些常量

#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.属性变量的声明

#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.布局

#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 ,它的实现如下:

#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

#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

//实现这个方法,跟前面找出最小的列高度的方法如出一辙
-(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这个属性

#import <UIKit/UIKit.h>

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

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

     2.自定义cell的实现文件

#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.net/213963.html原文链接:https://javaforall.net

(0)
上一篇 2025年7月16日 下午3:22
下一篇 2025年7月16日 下午4:01


相关推荐

  • usb转rs485测试软件,usb转rs485「建议收藏」

    usb转rs485测试软件,usb转rs485「建议收藏」usb转rs485电脑版驱动中还含有安装教程,在安装前可以先看看使用说明再安装。将USB转换线插入电脑的USB接口中,系统会提示检测到新设备并出现新硬件添加向导,选择从列表或指定位置安装,手动安装,找到刚才驱动的解压目录,让WINDOWS自动搜索更新驱动即可。usb转rs485软件功能1、支持的操作系统Windows2000/WindowsXP2、完全兼容USBV1.1和USBCDCV1….

    2022年4月27日
    69
  • 常用的算法和数据结构 面试_数据结构与算法面试题80道

    常用的算法和数据结构 面试_数据结构与算法面试题80道(1)红黑树的了解(平衡树,二叉搜索树),使用场景把数据结构上几种树集中的讨论一下:1.AVLtree定义:最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。…

    2022年8月18日
    10
  • Coze案例教程:如何解决数据绑定时页面不更新的问题?

    Coze案例教程:如何解决数据绑定时页面不更新的问题?

    2026年3月15日
    1
  • common sense security framework

    common sense security framework

    2021年9月4日
    58
  • siger获取 本机信息

    siger获取 本机信息sigar x86 winnt dll 文件拷贝到 Java nbsp SDK 目录的 binpublic nbsp static nbsp void nbsp main String nbsp args nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp try nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp System 信息 从 jvm 获取 nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp System setProperty java library path nbsp so

    2026年3月19日
    1
  • 实现Parcelable

    实现ParcelableActivity 之间通过 intent 传递 object 时 该 object 的 class 需要实现 Parcelable nbsp Interfacefor nbsp Parcel Classesimple

    2025年9月6日
    9

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号