自定义UICollectionViewLayout之瀑布流
目标效果
因为系统给我们提供的 UICollectionViewFlowLayout 布局类不能实现瀑布流的效果,如果我们想实现 瀑布流 的效果,需要自定义一个 UICollectionViewLayout 类,实现瀑布流效果。效果如右图。
依赖工具:
我们需要一个图片大小和图片地址的Josn数据, 和 SDWebImage图片加载的第三方工具
RootViewController.m
1 #import "RootViewController.h" 2 #import "DataModel.h" 3 #import "WaterFlowLayout.h" 4 #import "RootCell.h" 5 #import "UIImageView+WebCache.h" 6 7 @interface RootViewController ()<UICollectionViewDataSource, UICollectionViewDelegate, WaterFlowLayoutDelegate> 8 9 // 声明大数组存放所有的数据 10 @property (nonatomic, strong) NSMutableArray *allDataArray; 11 12 // 定义collectionView 13 @property (nonatomic, strong) UICollectionView *collectionView; 14 15 @end 16 17 @implementation RootViewController 18 19 // 懒加载 20 - (NSMutableArray *)allDataArray { 21 if (!_allDataArray) { 22 _allDataArray = [NSMutableArray array]; 23 } 24 return _allDataArray; 25 } 26 27 - (void)viewDidLoad { 28 [super viewDidLoad]; 29 // Do any additional setup after loading the view. 30 31 // 读取数据 32 [self loadData]; 33 34 // 初始化布局 35 [self initLayout]; 36 37 // 注册cell 38 [self.collectionView registerClass:[RootCell class] forCellWithReuseIdentifier:@"cell"]; 39 } 40 41 // 初始化布局 42 - (void)initLayout { 43 44 // 1.创建UICollectionView的布局样式对象 45 WaterFlowLayout *water = [[WaterFlowLayout alloc] init]; 46 CGFloat width = ([UIScreen mainScreen].bounds.size.width - 40) / 3; 47 water.itemSize = CGSizeMake(width, width); 48 // 设置内边距 49 water.sectionInsets = UIEdgeInsetsMake(10, 10, 10, 10); 50 // 设置间距 51 water.spacing = 10; 52 // 设置有多少列 53 water.numberOfColumn = 3; 54 // 设置代理 55 water.delegate = self; 56 57 // 2.布局UICollectionView 58 self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:water]; 59 self.collectionView.delegate = self; 60 self.collectionView.dataSource = self; 61 self.collectionView.backgroundColor = [UIColor whiteColor]; 62 [self.view addSubview:self.collectionView]; 63 64 } 65 66 // 读取数据 67 - (void)loadData { 68 69 // 第一步:获取文件路径 70 NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Data" ofType:@"json"]; 71 // 第二步:根据路径读取数据,转为NSData对象 72 NSData *data = [NSData dataWithContentsOfFile:filePath]; 73 // 第三步:根据json格式解析数据 74 NSArray *dataArray = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 75 76 // NSLog(@"%@", dataArray); 77 // 第四步:遍历数组,将数据转为model对象 78 for (NSDictionary *dict in dataArray) { 79 80 // 创建model对象 81 DataModel *model = [[DataModel alloc] init]; 82 // 使用KVC给model赋值 83 [model setValuesForKeysWithDictionary:dict]; 84 85 // 将model添加到大数组中 86 [self.allDataArray addObject:model]; 87 } 88 // NSLog(@"%@", self.allDataArray); 89 } 90 91 // 设置分区个数 92 - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { 93 return 1; 94 } 95 // 设置每个分区的item个数 96 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { 97 return self.allDataArray.count; 98 } 99 100 // 返回每一个item的cell对象 101 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { 102 103 RootCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; 104 105 // 设置cell数据 106 107 DataModel *model = self.allDataArray[indexPath.row]; 108 NSURL *url = [NSURL URLWithString:model.thumbURL]; 109 [cell.photoView sd_setImageWithURL:url]; 110 cell.backgroundColor = [UIColor orangeColor]; 111 return cell; 112 } 113 114 // 实现代理方法返回每一个item的高度 115 - (CGFloat)heightForItemAtIndexPath:(NSIndexPath *)indexPath { 116 117 DataModel *model = self.allDataArray[indexPath.row]; 118 CGFloat width = ([UIScreen mainScreen].bounds.size.width - 40) / 3; 119 // 计算item高度 120 CGFloat height = model.height / model.width * width; 121 return height; 122 123 } 124 125 @end
主视图文件,主要用于处理数据和布局页面
RootCell.h
1 #import <UIKit/UIKit.h> 2 3 @interface RootCell : UICollectionViewCell 4 5 @property (nonatomic, strong) UIImageView *photoView; 6 7 @end
RootCell.m
1 #import "RootCell.h" 2 3 @implementation RootCell 4 5 - (instancetype)initWithFrame:(CGRect)frame { 6 self = [super initWithFrame:frame]; 7 if (self) { 8 self.photoView = [[UIImageView alloc] init]; 9 [self.contentView addSubview:self.photoView]; 10 } 11 return self; 12 } 13 14 - (void)layoutSubviews { 15 16 self.photoView.frame = self.bounds; 17 18 } 19 20 @end
RootCell就是每一个Item的样式, 也就是一张张图片
WaterFlowLayout.h
1 #import <UIKit/UIKit.h> 2 3 @protocol WaterFlowLayoutDelegate <NSObject> 4 5 // 返回每一个item的高度 6 - (CGFloat)heightForItemAtIndexPath:(NSIndexPath *)indexPath; 7 8 @end 9 10 @interface WaterFlowLayout : UICollectionViewLayout 11 12 // item的大小,需要根据这个获取宽度 13 @property (nonatomic, assign) CGSize itemSize; 14 15 // 内边距的设置 16 @property (nonatomic, assign) UIEdgeInsets sectionInsets; 17 18 // item的间距(这里水平方向和垂直方向的间距一样) 19 @property (nonatomic, assign) CGFloat spacing; 20 21 // 列数 22 @property (nonatomic, assign) NSInteger numberOfColumn; 23 24 // 设置代理,用于获取item的高度 25 @property (nonatomic, weak) id<WaterFlowLayoutDelegate>delegate; 26 27 @end
WaterFlowLayout.m
1 #import "WaterFlowLayout.h" 2 3 @interface WaterFlowLayout () 4 5 // 声明私有属性 6 // 保存一共有多少个item 7 @property (nonatomic, assign) NSInteger numberOfItems; 8 9 // 保存计算好的每一个item的信息 10 @property (nonatomic, strong) NSMutableArray *itemAttributes; 11 12 // 保存每一列的高度 13 @property (nonatomic, strong) NSMutableArray *columnHeights; 14 15 16 // 声明私有方法 17 // 找到当前最长列 18 - (NSInteger)indexOfHeightestColumn; 19 20 // 找到当前最短列 21 - (NSInteger)indexOfShortestColumn; 22 23 @end 24 25 @implementation WaterFlowLayout 26 27 // 懒加载 28 - (NSMutableArray *)itemAttributes { 29 if (!_itemAttributes) { 30 _itemAttributes = [NSMutableArray array]; 31 } 32 return _itemAttributes; 33 } 34 35 - (NSMutableArray *)columnHeights { 36 if (!_columnHeights) { 37 _columnHeights = [NSMutableArray array]; 38 } 39 return _columnHeights; 40 } 41 42 // 找到当前最长列 43 - (NSInteger)indexOfHeightestColumn { 44 // 记录最长列的下标 45 NSInteger index = 0; 46 // 记录最长列的高度 47 CGFloat length = 0; 48 for (int i = 0; i < self.columnHeights.count; i++) { 49 // 将数组中的对象转为基本数值 50 CGFloat currentLength = [self.columnHeights[i] floatValue]; 51 if (currentLength > length) { 52 length = currentLength; 53 index = i; 54 } 55 } 56 return index; 57 } 58 59 // 找到当前最短列 60 - (NSInteger)indexOfShortestColumn { 61 NSInteger index = 0; 62 CGFloat length = MAXFLOAT; 63 for (int i = 0; i < self.columnHeights.count; i++) { 64 65 CGFloat currentLength = [self.columnHeights[i] floatValue]; 66 if (currentLength < length) { 67 length = currentLength; 68 index = i; 69 } 70 } 71 return index; 72 } 73 74 // 接下来重写三个方法 75 76 // 准备布局,在这里计算每个item的frame 77 - (void)prepareLayout { 78 79 // 拿到一共有多少个item 80 self.numberOfItems = [self.collectionView numberOfItemsInSection:0]; 81 // 每一列添加一个top高度 82 for (int i = 0; i < self.numberOfColumn; i++) { 83 // @() NSNumber字面量创建对象 84 self.columnHeights[i] = @(self.sectionInsets.top); 85 } 86 87 // 依次为每个item设置位置信息,并存储在数组中 88 for (int i = 0; i < self.numberOfItems; i++) { 89 90 // 1.找到最短列的下标 91 NSInteger shortestIndex = [self indexOfShortestColumn]; 92 // 2.计算X 目标X = 内边距左间距 + (宽 + item间距)*最短列下标 93 CGFloat detalX = self.sectionInsets.left + shortestIndex * (self.itemSize.width + self.spacing); 94 // 3.找到最短列的高度 95 CGFloat height = [self.columnHeights[shortestIndex] floatValue]; 96 // 4.计算Y 97 CGFloat detalY = height + self.spacing; 98 // 5.创建indexPath 99 NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0]; 100 101 // 6.调用代理方法计算高度 102 CGFloat itemHeight = 0; 103 if (_delegate && [_delegate respondsToSelector:@selector(heightForItemAtIndexPath:)]) { 104 itemHeight = [_delegate heightForItemAtIndexPath:indexPath]; 105 } 106 107 // 定义保存位置信息的对象 108 UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 109 110 // 7.生成frame 111 attributes.frame = CGRectMake(detalX, detalY, self.itemSize.width, itemHeight); 112 // 8.将位置信息添加到数组中 113 [self.itemAttributes addObject:attributes]; 114 115 // 9.更新这一列的高度 116 self.columnHeights[shortestIndex] = @(detalY + itemHeight); 117 } 118 119 } 120 121 // 返回UICollectionView的大小 122 - (CGSize)collectionViewContentSize { 123 124 // 求最高列的下标 125 NSInteger heightest = [self indexOfHeightestColumn]; 126 // 最高列的高度 127 CGFloat height = [self.columnHeights[heightest] floatValue]; 128 // 拿到collectionView的原始大小 129 CGSize size = self.collectionView.frame.size; 130 size.height = height + self.sectionInsets.bottom; 131 132 return size; 133 } 134 135 // 返回每一个item的位置信息 136 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { 137 return self.itemAttributes; 138 } 139 140 @end
WaterFlowLayout 就是我们自定义的 瀑布流 的文件
DataModel.h
1 #import <Foundation/Foundation.h> 2 3 @interface DataModel : NSObject 4 5 @property (nonatomic, copy) NSString *thumbURL; 6 7 @property (nonatomic, assign) float width; 8 9 @property (nonatomic, assign) float height; 10 11 12 @end
DataModel.m
1 #import "DataModel.h" 2 3 @implementation DataModel 4 5 // 防崩 6 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { 7 8 } 9 10 @end