UI基础 - UICollectionView 03:瀑布流

发布时间 2024-01-08 18:24:36作者: 低头捡石頭

■ 简言 

1. 实现瀑布流的方式有很多种,但是比较简单的是通过 UICollectionView 实现。瀑布流最重要的是布局:就是要选取最短的那一列来布局

2. 我们知道 UICollectionView 的相关的设置都是由 UICollectionViewLayoutAttributes 来完成的,每一个 cell 都对应的有一个 UICollectionViewLayoutAttributes 来设置它的属性。当我们在修改 UICollectionViewLayoutAttributes 的属性时, 实际上也就间接地修改了对应 cell 的相关属性!所以我们对 UICollectionView 的很多自定义就落在了 UICollectionViewLayoutAttributes 上面

3. 要完成 UICollectionView 的布局,就需要设置它的属性 UICollectionViewLayout!当使用 storyboard 时是默认 UICollectionViewFlowLayout:它是继承自 UICollectionViewLayout,由系统实现的一种布局。注:建议使用 UICollectionViewLayout 来自定义我们想要的布局

■ 工作原理

1. UICollectionView 每次需要重新布局的时

首先会调用这个方法 prepareLayout()。所以 Apple 建议我们可以重写这个方法来为自定义布局做一些准备的操作,在 cell 比较少的情况下,我们一般都可以在这个方法里面计算好所有的 cell 布局,并且缓存下来,在需要的时候直接取相应的值即可

其次会调用 layoutAttributesForElementsInRect (rect: CGRect) 方法获取到 rect 范围内的 cell 的所有布局。这个 rect 和 collectionView 的 bounds 不一样,size 可能比 collectionView 大一些,这样设计也许是为了缓冲。Apple 要求这个方法必须重写,并且提供相应 rect 范围内的 cell 的所有布局的 UICollectionViewLayoutAttributes

■ 具体实现

1. 下面代码中将 Cell 分成 4 列,共 5 个 Cell,实现瀑布流效果

// - LayoutDemo.h : 继承自 UICollectionViewLayout

 1 #import <UIKit/UIKit.h>
 2 // 自定义一个协议,返回图片高度
 3 @protocol LayoutDelegate <NSObject>
 4 - (CGFloat)collectionView:(UICollectionView *_Nonnull)collectionView
 5                    layout:(UICollectionViewLayout *_Nonnull)collectionViewLayout
 6                     width:(CGFloat)width
 7  heightForItemAtIndexPath:(nonnull NSIndexPath *)indexPath;
 8 @end
 9 
10 @interface LayoutDemo : UICollectionViewLayout
11 
12 @property(nonatomic, assign)int numberOfColumns; // 列数
13 @property(nonatomic, assign)id <LayoutDelegate> _Nonnull delegate;
14 
15 @end

// - LayoutDemo.m

  1 #import "LayoutDemo.h"
  2 @interface LayoutDemo()
  3 @property(nonatomic, strong)NSMutableArray *attributeArray;
  4 // 整个 collectionView 的 contenView 的高度
  5 @property(nonatomic, assign)CGFloat contentHeight;
  6 // 间距
  7 @property(nonatomic, assign)CGFloat cellMargin;
  8 @end
  9 
 10 @implementation LayoutDemo
 11 
 12 - (instancetype)init {
 13     self = [super init];
 14     if (self) {
 15         _attributeArray = [NSMutableArray array];
 16         _numberOfColumns = 2;
 17         _cellMargin = 5.0f;
 18         _contentHeight = 0.0f;
 19     }
 20     return self;
 21 }
 22 
 23 // 计算 item 的宽度
 24 - (CGFloat)itemWidth{
 25     // 所有边距的和
 26     // 两列时有三个边距, 三列时有四个边距......
 27     CGFloat allMargin = (_numberOfColumns + 1) * _cellMargin;
 28     // 除去边界之后的总宽度
 29     CGFloat allWidth = CGRectGetWidth(self.collectionView.bounds) - allMargin;
 30     // 列的宽度,也就是 itemWidth
 31     return allWidth / _numberOfColumns;
 32 }
 33 
 34 // UICollectionView 在进行 UI 布局前,会通过这个类的对象获取相关的布局信息,就是 UICollectionViewLayoutAttributes
 35 // 该类将这些布局信息全部存放在了一个数组中,通过 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect 返回该数组
 36 
 37 // 重写该方法:计算 item 布局
 38 - (void)prepareLayout {
 39     
 40     // 定义变量记录高度最小的列:初始为第 0 列高度最小
 41     NSInteger shortestColumn = 0;
 42     // 存储每一列的总高度
 43     NSMutableArray *columnHeightArray = [NSMutableArray array];
 44     // 列的初始高度:边距高度
 45     for (int i = 0; i < _numberOfColumns; i++) {
 46         [columnHeightArray addObject:@(_cellMargin)];
 47     }
 48     
 49     // 遍历 collectionView 中第 0 区中的所有 item
 50     for (int i = 0; i < [self.collectionView numberOfItemsInSection:0]; i++) {
 51         
 52         NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
 53         // 获取布局属性对象,就是每个 item 的布局属性
 54         UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
 55         // 将布局属性放入数组中
 56         [_attributeArray addObject:layoutAttributes];
 57         
 58         // 开始设置每个 item  的位置(x, y, width, height)
 59         // 比如一共两列,现在要放一张图片上去,需要放到高度最小的那一列
 60         // 假设第 0 列最短,那么 item 的 x 坐标就是从一个边距宽度那里开始
 61         
 62         // 橫坐标
 63         CGFloat x = (self.itemWidth + _cellMargin) * shortestColumn + _cellMargin;
 64         // 纵坐标就是 总高度数组 中最小列对应的高度
 65         CGFloat y = [columnHeightArray[shortestColumn] floatValue];
 66         // 宽度
 67         CGFloat width = self.itemWidth;
 68         
 69         // 这里给自定义的类声明了一个协议
 70         // 通过协议得到图片的高度,调用时机就是需要 item 高度的时候
 71         // 将 Item 的宽度传给代理(ViewController),VC 计算好高度后将高度返回给自定义类
 72         CGFloat height = [self.delegate collectionView:self.collectionView
 73                                                 layout:self
 74                                                  width:self.itemWidth
 75                               heightForItemAtIndexPath:indexPath];
 76         // item 的位置信息
 77         layoutAttributes.frame = CGRectMake(x, y, width, height);
 78         
 79         // 现在开始更新总高度数组
 80         columnHeightArray[shortestColumn] = @([columnHeightArray[shortestColumn] floatValue] + height + _cellMargin);
 81         // 整个内容的高度,通过比较得到较大值作为整个内容的高度
 82         self.contentHeight = MAX(self.contentHeight, [columnHeightArray[shortestColumn] floatValue]);
 83         
 84         // 刚才放了一个 item 上去,现在开始找出高度最小的那个列
 85         for (int i = 0; i < _numberOfColumns; i++) {
 86             // 当前列的高度:刚才添加 item 的那一列
 87             CGFloat currentHeight = [columnHeightArray[shortestColumn] floatValue];
 88             // 取出第 i 列中存放列高度
 89             CGFloat height = [columnHeightArray[i] floatValue];
 90             if (currentHeight > height) {
 91                 // 第 i 列高度最低时赋值给高度最小的列
 92                 shortestColumn = i;
 93             }
 94         }
 95     }
 96 }
 97 
 98 // 返回的是每个 item 对应的布局属性
 99 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
100     return _attributeArray;
101 }
102 
103 // 返回 CollectionView 的滚动范围
104 - (CGSize)collectionViewContentSize {
105     return CGSizeMake(0, _contentHeight);
106 }
107 
108 @end

// - ViewController.m

 1 #import "ViewController.h"
 2 #import "LayoutDemo.h"
 3 #import <AVFoundation/AVFoundation.h> // 渲染图片需要的头文件
 4 @interface ViewController ()<UICollectionViewDataSource,UICollectionViewDelegate,LayoutDelegate>
 5 @property(nonatomic,strong)NSMutableArray *imagesArray;
 6 @end
 7 @implementation ViewController
 8 
 9 - (void)viewDidLoad {
10     [super viewDidLoad];
11     
12     // LayoutDemo
13     LayoutDemo *layout = [[LayoutDemo alloc] init];
14     layout.numberOfColumns = 4;
15     layout.delegate = self;
16     
17     // UICollectionView
18     UICollectionView *collect = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:layout];
19     collect.delegate = self;
20     collect.dataSource = self;
21     [collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"];
22     [self.view addSubview:collect];
23 }
24 
25 - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
26     return 1;
27 }
28 
29 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
30     return 5;
31 }
32 
33 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
34     UICollectionViewCell *cell  = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellid" forIndexPath:indexPath];
35     cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1];
36     
37     UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height)];
38     [cell addSubview:imageView];
39     imageView.image = self.imagesArray[indexPath.row];
40     
41     return cell;
42 }
43 
44 #pragma mark - <LayoutDelegate>
45 - (CGFloat)collectionView:(UICollectionView *)collectionView
46                    layout:(UICollectionViewLayout *)collectionViewLayout
47                     width:(CGFloat)width
48  heightForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
49     
50     UIImage *image = self.imagesArray[indexPath.row];
51     // 根据传过来的宽度来设置一个合适的矩形,
52     // 高度设为 CGFLOAT_MAX 表示以宽度来计算高度
53     CGRect boundingRect = CGRectMake(0, 0, width, CGFLOAT_MAX);
54     // 通过系统函数来得到最终的矩形。需要引入头文件 <AVFoundation/AVFoundation.h>
55     CGRect imageCurrentRect = AVMakeRectWithAspectRatioInsideRect(image.size, boundingRect);
56     return imageCurrentRect.size.height;
57 }
58 
59 #pragma mark - 懒加载
60 // 存放 5 张图片
61 - (NSMutableArray *)imagesArray{
62     if(!_imagesArray){
63         _imagesArray = [NSMutableArray array];
64         for (int i = 1; i < 6; i ++) {
65             UIImage *aImage = [UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]];
66             [_imagesArray addObject:aImage];
67         }
68     }
69     return _imagesArray;;
70 }
71 
72 @end

运行效果