UI基础 - UICollectionView 03:瀑布流
■ 简言
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
运行效果