iOS瀑布流详细介绍

作者: Macgx | 来源:发表于2016-08-06 20:53 被阅读586次
    • 传统!!!依然是效果演示
    瀑布流效果.gif
    • 特点:可以自由设置瀑布流的总列数(效果演示为2列)

    虽然iphone手机的系统相册没有使用这种布局效果,瀑布流依然是一种很常见的布局方式!!!下面来详细介绍如何实现这种布局.

    • 首先使用的类是UICollectionView
    • 我们要做的是自定义UICollectionViewCell和UICollectionViewLayout
    1. 自定义UICollectionViewCell类,只需要一个UIImageView即可,frame占满整个cell.
    1. 重点是自定义UICollectionViewLayout,注意一定要继承于UICollectionViewLayout,千万别继承于UIColletionViewFlowLayout.
    2. 另外还需要计算图片高度.
    • 为什么要自定义UICollectionViewLayout ?

    因为我们需要设置每个item的高度以及位置, 注意这里是位置, 我们真的会设置每个item的位置的相信我!!!自定义UICollectionViewLayout必须要重写三个协议方法,后面会讲到.

    • 为什么要计算图片高度 ?

    因为图片宽度固定,所以需要按照图片的比例来计算高度,使图片等比例显示.这样的好处是,妈妈再也不用担心我的照片被拉伸的奇形怪状了...而且还需要用图片的高度来计算整个CollectionView的contentSize...打完收工!!!

    • 主菜来了!!!

    以下内容均在自定义的CustomCollectionViewLayout类里边

    //自定义UICollectionViewLayout必须要重写的三个协议方法
    //1.计算每个item的大小和位置
    - (void)prepareLayout;
    //2.返回每个item的布局属性
    - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
    //3.返回collectionView的总高度
    - (CGSize)collectionViewContentSize;
    

    可以看到第三个方法使用了Nullability和泛型,系统的方法都添加了iOS 9新特性,不了解的朋友可以点击这里来查看iOS 9新特性的说明.

    • 通过上边的三个方法名我们可以大致了解需要去做什么.说一下第二个方法,需要返回一个数组,数组存放布局属性(UICollectionViewLayoutAttributes)对象.那么我们需要写一个属性数组(attributesArray),将布局属性放入这个属性数组并返回.
    定义存放布局属性的可变数组
    • 还需要什么呢 ?看了文章开头的朋友应该注意到了,设置瀑布流的列数当然得有个属性(numberOfColumns)来表示列数.
    请注意,因为要在外部设置列数,所以这个属性需要写在自定义类的.h文件中
    • 另外为了方便,定义一个属性(itemWidth)来表示item的宽度
    • 再定义一个属性(contentHeight)来表示整个collectionView的contenView的高度
    CustomCollectionViewLayout.m
    • 首先是初始化,并没有什么问题
    - (instancetype)init {
        self = [super init];
        if (self) {
            _attributesArray = [NSMutableArray array];
            // 默认值设置为2列
            _numberOfColumns = 2;
            _contentHeight = 0.0f;
            _cellMargin = 5.0f;/**< 用来表示间距的属性 */
        }
        return self;
    }
    
    • 然后是getter方法,只需要使用点语法即可得到itemWidth的值(因为它就是固定的)
    - (CGFloat)itemWidth {
        //所有边距的和.两列时有三个边距, 三列时有四个边距,逻辑强大就是好...
        CGFloat allMargin = (_numberOfColumns + 1) * _cellMargin;
        //除去边界之后的总宽度
        CGFloat noMarginWidth = CGRectGetWidth(self.collectionView.bounds) - allMargin;
        //出去边距的总宽度除以列数得到每一列的宽度(也就是itemWidth)
        return noMarginWidth / _numberOfColumns;
    }
    

    ---接下来是难点---

    • 必须重写的第一个方法
    - (void)prepareLayout {
        // 定义变量记录高度最小的列,初始为第0列高度最小.
    #pragma mark - 注意这个是从0开始算的啊!!!
        NSInteger shortestColumn = 0;
    #pragma mark - 注意这个是从0开始算的啊!!!
        // 存储每一列的总高度.因为添加图片的列高度会变,所以需要定义一个数组来记录列的总高度.
        NSMutableArray *columnHeightArray = [NSMutableArray array];
        // 设置列的初始高度为边距的高度,没毛病!!!
        for (int i = 0; i < _numberOfColumns; i++) {
            // 所有列初始高度均设置为cell的间距
            [columnHeightArray addObject:@(_cellMargin)];
        }
        // 遍历collectionView中第 0 区中的所有item
        for (int i = 0; i < [self.collectionView numberOfItemsInSection:0]; i++) {
            //需要用到这个玩意,提前拿到.
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
            // 创建系统需要的布局属性对象,看后边的参数就知道这就是每个item的布局属性了
            UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
            // 将布局属性放入数组中,这个数组当然是一开始定义的布局属性数组了
            [_attributesArray addObject:layoutAttributes];
            // 设置每个item的位置(x, y, width, height)
            // 横坐标的起始位置
    #pragma mark - 比如一共两列,现在要放一张图片上去,需要放到高度最小的那一列.
    #pragma mark - 假设第0列最短,那么item的x坐标就是从一个边距宽度那里开始.
    #pragma mark - (itemWidth + cellMargin)为一个整体
            CGFloat x = (self.itemWidth + _cellMargin) * shortestColumn + _cellMargin;
            // 纵坐标就是 总高度数组 中最小列对应的高度
    #pragma mark - 图片始终是添加在高度最小的那一列
            CGFloat y = [columnHeightArray[shortestColumn] floatValue];/**<注意类型转换 */
            // 宽度没什么好说的
            CGFloat width = self.itemWidth;
    #pragma mark - 这里给自定义的类声明了一个协议,通过协议得到图片的高度,调用时机就是需要item高度的时候
    #pragma mark - 将Item的宽度传给代理人(ViewController),VC计算好高度后将高度返回给自定义类
    #pragma mark - 也就是↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
            CGFloat height = [self.delegate collectionView:self.collectionView
                                                    layout:self
                                                     width:self.itemWidth
                                  heightForItemAtIndexPath:indexPath];
            // 大功告成,设置item的位置信息,没什么好说
            layoutAttributes.frame = CGRectMake(x, y, width, height);
            // 上边废了半天劲放了一个item上去了,总高度数组是不是该更新一下数据了
            columnHeightArray[shortestColumn] = @([columnHeightArray[shortestColumn] floatValue] + height + _cellMargin);
            // 整个内容的高度,通过比较得到较大值作为整个内容的高度
            self.contentHeight = MAX(self.contentHeight, [columnHeightArray[shortestColumn] floatValue]);
            // 刚才放了一个item上去,那么此时此刻哪一列的高度比较低呢
            for (int i = 0; i < _numberOfColumns; i++) {
                //当前列的高度(刚才添加item的那一列)
                CGFloat currentHeight = [columnHeightArray[shortestColumn] floatValue];
                // 取出第i列中存放列高度
                CGFloat height = [columnHeightArray[i] floatValue];
                if (currentHeight > height) {
                    //第i列高度(height)最低时,高度最低的列(shortestColumn)当然就是第i列了
                    shortestColumn = i;
                }
            }
        }
    // 思考下只使用上边的代码会出现什么问题
    // 并不能影响心情,请不要恐慌...
    // 提示:这个方法会被多次调用,数组
    }
    

    ---难点已经结束---

    没有理解的朋友请重点看上边的难点方法.

    • 必须重写的第二个方法
    // 2.返回的是, 每个item对应的布局属性
    - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
        return _attributesArray;
    }
    
    • 必须重写的第三个方法
    // 3.返回CollectionView的滚动范围
    - (CGSize)collectionViewContentSize {
        return CGSizeMake(0, _contentHeight);
    }
    
    • ViewController里边关于CollectionView的创建和协议方法就没什么好说的了.
    • 看下自定义类CustomCollectionViewLayout的创建以及属性的赋值情况:
    CustomCollectionViewLayout *layout = [[CustomCollectionViewLayout alloc] init];
    layout.numberOfColumns = 2;/**< 在ViewController里设置瀑布流的列数,2列或3列为最佳 */
    layout.delegate = self;/**< 指定VC为计算高度协议方法的代理人 */
    
    • 再看下协议方法的实现部分(在ViewController.m中实现)
    - (CGFloat)collectionView:(UICollectionView *)collectionView
                       layout:(UICollectionViewLayout *)collectionViewLayout
                        width:(CGFloat)width
     heightForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
        UIImage *image = _imagesArray[indexPath.row];
        // 根据传过来的宽度来设置一个合适的矩形, 高度设为CGFLOAT_MAX表示以宽度来计算高度
        CGRect boundingRect = CGRectMake(0, 0, width, CGFLOAT_MAX);
        // 通过系统函数来得到最终的矩形,需要引入头文件
        // #import <AVFoundation/AVFoundation.h>
        CGRect imageCurrentRect = AVMakeRectWithAspectRatioInsideRect(image.size, boundingRect);
        return imageCurrentRect.size.height;
    }
    

    到这里呢,瀑布流就算是结束了,有兴趣的朋友可以自己动手试一下.

    没明白的话可以在评论区提问

    相关文章

      网友评论

      • hlxlx:有个问题,prepareLayout方法中,为什么是遍历0 区 的item?
        Macgx:只有一个分区
      • 可爱丶洪水猛兽:网络请求下来的图片数组,
      • 可爱丶洪水猛兽:你添加高度到最小的那一列,其中column是哪里来的?
        Macgx:@可爱丶洪水猛兽 加进去的是图片数组还是你把网络请求任务加到里边了?
        可爱丶洪水猛兽:@嘴角微寒 亲,我网络获取数据20条,全部加进去他就会卡死界面呀,我只取第一条的话就能加进去。怎么破?
        Macgx:@可爱丶洪水猛兽 感谢提醒, column就是方法开头所写的shortestColumn.
      • Hope_Man:一个问题:像淘宝上衣服展示的时候,图片根据高度随机排布,当用户点击进入,该如何获取到图片的对应信息呢?
        Macgx:@baobaoyuan 方法挺多,比如给图片设置一个tag值,点击的时候根据tag获取图片就行
      • zweic:点单张图片可以浏览吗
        Macgx:@zweic 不行, 这个是一种整体布局的显示效果.单张浏览的话按照图片比例显示就可以, 不需要瀑布流.
      • 我与太阳肩并肩:博主写的太赞了, 希望继续写出这么好的文章
        :+1:
      • xxttw:注释很详细

      本文标题:iOS瀑布流详细介绍

      本文链接:https://www.haomeiwen.com/subject/szwojttx.html