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


相关推荐

  • Navicat mysql报错 1142 – SELECT command denied to user ‘xxx‘@‘localhost‘ for table ‘user‘

    Navicat mysql报错 1142 – SELECT command denied to user ‘xxx‘@‘localhost‘ for table ‘user‘话我就撂这儿了,只要你认真看完,你就能解决。困了我一天一夜的问题终于解决了,问题也不知道是怎么产生的,点击“用户”或者修改“information_schema”的值就会提示错误,似乎是因为权限不足,错误入下图。首先你要知道数据库的用户是怎么回事。每个数据库都有账号密码,连接特定的数据库需要对应的账号密码,这个很容易理解,PHP里的mysqli_connect你们也用的多了。主机上的…

    2022年10月1日
    10
  • Raid5磁盘阵列修复方法介绍「建议收藏」

    Raid5磁盘阵列修复方法介绍「建议收藏」  在RAID5中,数据条带化后存储在分布式奇偶校验的多个磁盘上。分布式奇偶校验的条带化意味着它将奇偶校验信息和条带化数据分布在多个磁盘上,这样会有很好的数据冗余。而有时候raid5磁盘阵列损坏,我们该如何修复呢?  1.若单个硬盘失效,尝试热插拔,即拔下来再插上去;如果不能解决,则进入RAID配置界面,将该硬盘进行ForceOnLine操作;如果不能解决,尝试更换-其它硬盘…

    2022年4月28日
    1.4K
  • rolling在舞蹈里是什么意思_机械舞和街舞有啥区别

    rolling在舞蹈里是什么意思_机械舞和街舞有啥区别原标题:这,就是街舞中的那些“Swag”十足的舞蹈类型,你了解吗?世界越来越小了,人们越靠越近,视野越来越广,局限于逼仄的小空间的时代已经一去不复返了。现在,一推门,迎面就是全世界。自去年开始,嘻哈文化开始通过综艺节目进入公众视野(中国有嘻哈),这种来自大洋彼岸的“陌生文化”随即引起了广泛的关注。尽管作为“第一个吃螃蟹的人”,《中国有嘻哈》的最终结果不尽如人意,但却为后来的综艺制作人提供了全新的视…

    2025年5月28日
    5
  • Java html转word_html文件转换成excel

    Java html转word_html文件转换成excel使用aspose的原因:1.使用简单,功能强大2.可以自动将html中可以访问的img标签存入word文档中3.可以轻松实现HTML中的样式转换到word文档中首先使用的jar包是:aspose-words-14.9.0-jdk16.jar这个可以在网上找到激活成功教程版以下是代码:1:读取asposelicensepublicstaticbooleangetAsposeWordLice

    2022年10月10日
    7
  • robots.txt详解[通俗易懂]

    robots.txt详解[通俗易懂]怎样查看robots文件?浏览器输入主域名/robots.txtrobots.txt的作用robots.txt文件规定了搜索引擎抓取工具可以访问网站上的哪些网址,并不禁止搜索引擎将某个网页纳入索引。如果想禁止索引(收录),可以用noindex,或者给网页设置输入密码才能访问(因为如果其他网页通过使用说明性文字指向某个网页,Google在不访问这个网页的情况下仍能将其网址编入索引/收录这个网页)。robots.txt文件主要用于管理流向网站的抓取工具流量,通常用于阻止Google.

    2022年5月1日
    48
  • eclipse svn冲突怎么解决_键位冲突怎么解决

    eclipse svn冲突怎么解决_键位冲突怎么解决点击打开链接

    2022年10月14日
    4

发表回复

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

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