iOS瀑布流之横-纵瀑布流

一、开篇

1.瀑布流设计讨论

1.应用场景:在我们app开发中,瀑布流一般应用于大数据展示时,比如淘宝搜索页面、蘑菇街app、各大直播app主播列表页面等等。

2.设计思路:我们首要考虑UICollectionView,因为UICollectionView特殊性和可复用性。因为瀑布流每个格子item大小不同,需要计算每个item的宽高,就需要自定义UICollectionViewLayout。

2.瀑布流分类

我把瀑布流分为两种:垂直瀑布流、水平瀑布流。

1.垂直瀑布流:在我们app上实现的瀑布流一般是垂直瀑布流,也就是列数可设定,item宽度由列数决定,从上往下布局。

2.水平瀑布流:我在搜索网页百度图片时,看到百度图片的布局从而想到的一种布局方式。每行的高度是可设定的,item高度由图片大小决定,比例缩放。

3.瀑布流样式展示

垂直瀑布.jpeg

水平瀑布.jpeg

二、具体实现

1.创建一个UICollectionViewCell,它上面只有一个imageView。

// CollectionViewCell.m
- (instancetype)initWithFrame:(CGRect)frame {
    
    if (self = [super initWithFrame:frame]) {
        _imageView = [[UIImageView alloc] init];
        _imageView.frame = self.bounds;
        [self addSubview:_imageView];
    }
    return self;
}

2.我们需要自定义一个layout,并且暴露一些可设置的属性用来控制瀑布流的对应展示,让瀑布流可用性更强大一些。

2.1 头文件:DYTWaterflowLayout.h
@interface DYTWaterflowLayout : UICollectionViewLayout
/**
 *  行高(水平瀑布流时),默认为100
 */
@property (nonatomic, assign) CGFloat rowHeight;
/**
 *  单元格宽度(垂直瀑布流时)
 */
@property (nonatomic, assign, readonly) CGFloat itemWidth;
/**
 *  列数 : 默认为3
 */
@property (nonatomic, assign) NSInteger numberOfColumns;

/**
 *  内边距 : 每一列之间的间距 (top, left, bottom, right)默认为{10, 10, 10, 10};
 */
@property (nonatomic, assign) UIEdgeInsets insets;

/**
 *  每一行之间的间距 : 默认为10
 */
@property (nonatomic, assign) CGFloat rowGap;

/**
 *  每一列之间的间距 : 默认为10
 */
@property (nonatomic, assign) CGFloat columnGap;

/**
 *  高度数组 : 存储所有item的高度
 */
@property (nonatomic, strong) NSArray *itemHeights;

/**
 *  宽度数组 : 存储所有item的宽度
 */
@property (nonatomic, strong) NSArray *itemWidths;

/**
 *  瀑布流类型 : 分为水平瀑布流 和 垂直瀑布流
 */
@property (nonatomic, assign) DirectionType type;

@end
2.2 .m文件:DYTWaterflowLayout.m属性声明
@interface DYTWaterflowLayout()

@property (nonatomic, strong) NSMutableArray *itemAttributes; // 存放每个cell的布局属性

// 垂直瀑布流相关属性
@property (nonatomic, strong) NSMutableArray *columnsHeights; // 每一列的高度(count=多少列)
@property (nonatomic, assign) CGFloat maxHeight; // 最长列的高度(最大高度)
@property (nonatomic, assign) CGFloat minHeight; // 最短列的高度(最低高度)
@property (nonatomic, assign) NSInteger minIndex; // 最短列的下标
@property (nonatomic, assign) NSInteger maxIndex; // 最长列的下标

// 水平瀑布流相关属性
@property (nonatomic, strong) NSMutableArray *columnsWidths; // 每一行的宽度(count不确定)
@property (nonatomic, assign) NSInteger tempItemX; // 临时x : 用来计算每个cell的x值
@property (nonatomic, assign) NSInteger maxRowIndex; //最大行

@end
2.3 .m文件:DYTWaterflowLayout.m主要方法实现
// 
#pragma mark -- 系统内部方法
/**
 *  重写父类布局
 */
- (void)prepareLayout {
    
    [super prepareLayout];
    // (水平瀑布流时)重置最大行
    if ((self.type == HorizontalType)) {
        self.maxRowIndex = 0;
    }
    
    if (self.type == VerticalType) {
        // (垂直瀑布流时)重置每一列的高度
        [self.columnsHeights removeAllObjects];
        for (NSUInteger i = 0; i < self.numberOfColumns; i++) {
            [self.columnsHeights addObject:@(self.insets.top)];
        }
    }
    
    // 计算所有cell的布局属性
    [self.itemAttributes removeAllObjects];
    NSUInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    self.tempItemX = self.insets.left;
    for (NSUInteger i = 0; i < itemCount; ++i) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        if (self.type == VerticalType) {
            [self setVerticalFrame:indexPath];
        }else if ((self.type == HorizontalType)){
            [self setHorizontalFrame:indexPath];
        }
    }
}

/**
 *  水平瀑布:设置每一个attrs的frame,并加入数组中
 */
- (void)setHorizontalFrame:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    CGFloat w = [self.itemWidths[indexPath.item] floatValue];
    CGFloat width = w + self.columnGap;
    CGFloat h = (self.rowHeight == 0) ? 100 : self.rowHeight;
    
    /**
     *  如果当前的x值+当前cell的宽度 超出了 屏幕宽度,那么就要换行了。
     *  换行操作 : 最大行+1,tempItemX重置为10(self.insets.left)。
     */
    if (self.tempItemX + w > [UIScreen mainScreen].bounds.size.width) {
        self.maxRowIndex++;
        self.tempItemX = self.insets.left;
    }
    CGFloat x = self.tempItemX;
    CGFloat y = self.insets.top + self.maxRowIndex * (h + self.rowGap);
    attrs.frame = CGRectMake(x, y, w, h);
    
    /**
     * 注:1.cell的宽度和高度算起来比较简单 : 宽度由外部传进来,高度固定为rowHeight(默认为100)。
     *    2.cell的x : 通过tempItemX算好了。
     *    3.cell的y : minHeight最短列的高度,也就是最低高度,作为当前cell的起始y,当然要加上行之间的间隙。
     */
    
    NSLog(@"%@",NSStringFromCGRect(attrs.frame));
    [self.itemAttributes addObject:attrs];
    self.tempItemX += width;
}

/**
 *  垂直瀑布:设置每一个attrs的frame,并加入数组中
 */
- (void)setVerticalFrame:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    // cell的frame
    CGFloat w = self.itemWidth;
    CGFloat h = [self.itemHeights[indexPath.item] floatValue];
    CGFloat x = self.insets.left + self.minIndex * (w + self.columnGap);
    CGFloat y = self.minHeight + self.rowGap;
    attrs.frame = CGRectMake(x, y, w, h);
    
    /**
     * 注:1.cell的宽度和高度算起来比较简单 : 宽度固定(itemWidth已经算好),高度由外部传进来
     *    2.cell的x : minIndex最短列作为当前列。
     *    3.cell的y : minHeight最短列的高度,也就是最低高度,作为当前cell的起始y,当然要加上行之间的间隙。
     */
    
    // 更新数组中的最大高度
    self.columnsHeights[self.minIndex] = @(CGRectGetMaxY(attrs.frame));
    NSLog(@"%@",NSStringFromCGRect(attrs.frame));
    [self.itemAttributes addObject:attrs];
}

/**
 *  返回collectionView的尺寸
 */
- (CGSize)collectionViewContentSize {
    CGFloat height;
    if (self.type == HorizontalType) {
        CGFloat rowHeight = (self.rowHeight == 0) ? 100 : self.rowHeight;
        height = self.insets.top + (self.maxRowIndex+1) * (rowHeight + self.rowGap);
    }else {
        height = self.maxHeight;
    }
    return CGSizeMake(self.collectionView.frame.size.width, height);
}

/**
 *  所有元素(比如cell、补充控件、装饰控件)的布局属性
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.itemAttributes;
}
3.1 直接在控制器里使用
    // 设置布局
    DYTWaterflowLayout *layout = [[DYTWaterflowLayout alloc]init];
    layout.type = _type;
    // 设置相关属性(不设置的话也行,都有相关默认配置)
    layout.numberOfColumns = 3;
    layout.columnGap = 10;
    layout.rowGap = 10;
    layout.insets = UIEdgeInsetsMake(10, 10, 10, 10);
    layout.rowHeight = 100;
    self.collectionView.collectionViewLayout = self.waterflowLayout = layout;
3.2 (举例)垂直瀑布流时,SDWebImage获取图片block里的具体实现
if (weakSelf.heights.count < weakSelf.allImageUrls.count) {
   // 根据图片原始比例 计算 当前图片的高度(宽度固定)
   CGFloat scale = image.size.height / image.size.width;
   CGFloat width = weakSelf.waterflowLayout.itemWidth;
   CGFloat height = width * scale;
   NSNumber *heightNum = [NSNumber numberWithFloat:height];
   [weakSelf.heights addObject:heightNum];
}
if (weakSelf.heights.count == weakSelf.allImageUrls.count) {
   // 赋值所有cell的高度数组itemHeights
   weakSelf.waterflowLayout.itemHeights = weakSelf.heights;
   [weakSelf.collectionView reloadData];
}
3.3 UICollectionViewDataSource中要注意的点
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    cell.imageView.image = self.picImageArr[indexPath.item];
    // 注:非常关键的一句,由于cell的复用,imageView的frame可能和cell对不上,需要重新设置。
    cell.imageView.frame = cell.bounds;
    cell.backgroundColor = [UIColor orangeColor];
    return cell;
}

三、总结

1.瀑布流使用场景比较广泛,也是常用的技术之一,我也是又回顾了一遍,并且总结了整体的思路决定分享出来,结尾有demo,童鞋们可以自行下载。另外我参考的资料链接也会贴出,供大家研究比对。
2.水平瀑布流还没达到百度图片搜索的那种效果,右边距离屏幕间隙太大了,所以影响美观。后续会继续研究,期待有所突破。
3.有种情况是后台直接给图片的所有数据给我们,包括url、图片宽高等等,其实这样就是后台已经做好了图片的顺序优化处理。不过我们可以自己研究一下这个排序思路。如何达到右边间隙几乎相同,比如都为10。
4.当然有疑惑的地方可以留言或者直接私信我,我们可以一起讨论。

四、总结最终实现效果:

瀑布流.gif

本文章demo:
瀑布流Demo
参考相关文章:
iOS--瀑布流的实现 -- 作者Go_Spec
iOS 瀑布流基本实现 -- 作者iOS_成才录

额。。。如果想知道图片里的小姐姐是谁,请直接在文章下面留言。因为我想暂时留点悬念给大家。(皮一下就很开心😄)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 历时几天,“油腻的中年”这个热点应该已经过去了。不是我清高,是今天才有时间静下心来写这个选题。 追热点滞后也不全是...
    水晶陪你阅读 825评论 1 8
  • 爱,是一种追求。同时,爱更是一种能力,一种是否能给予别人爱,而自己内心也充盈着爱和喜悦的能力。“利他爱己,...
    萨希儿的风阅读 1,157评论 0 1
  • 肯定法:1.今天早上生活老师发消息叫宝贝起床去早锻炼,宝贝就自己起来了,已经持续两个星期了,我说:宝贝你从一开始要...
    跟儿子学习成长之路阅读 258评论 0 0
  • 我曾经说过,我不是一个真正的吃货。为什么呢? 其一,我对美食没有特别的渴求。真正的吃货应该是一有机会就全城搜索美食...
    艾普萝妮阅读 213评论 0 2