UICollectionview是iOS6之后引入的UI控件,继承自UIScrollVie。它和UITableview有着许多的相似之处,但是它是一个比UITableView更加强大的一个视图控件,使用过程中需要实现数据源以及代理方法,其特点如下:
- 系统自带的流水布局支持水平和垂直两种方式的布局;
- 通过layout配置方式进行布局;
- collectionview中item的大小和位置可以自定义;
- 可以自定义一套layout的布局方案
(1)遵循两个协议
- 数据源协议
UICollectionViewDataSource
- 代理方法协议
UICollectionViewDelegate
(2)注册cell
[collectionView registerClass:[MyCollectionViewCell class] forCellWithReuseIdentifier:@"cellID"];
(3)布局类
系统提供了两个布局类:
-
UICollectionViewLayout
:是一个抽象类,我们在自定义布局的时候可以继承此类,并在此基础上设置布局信息。 -
UICollectionViewFlowLayout
:继承于UICollectionViewLayout
,是系统写好的布局类,该类为我们提供了一个简单的布局样式。假如我们只需要一个特别简单的网格布局或者流水布局,可以直接使用它。
1.简单使用
//
// ViewController.m
// DJTestDemo
//
// Created by admin on 2021/6/8.
//
#import "ViewController.h"
#import "DJCollectionViewCell.h"
@interface ViewController ()<UICollectionViewDelegate,UICollectionViewDataSource>
@property(nonatomic,strong)UICollectionView *collrctionView;
@end
@implementation ViewController
-(void)viewDidLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
[self.view addSubview:self.collrctionView];
}
#pragma mark - UICollectionViewDelegate
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 2;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//创建item 从缓存池中拿 Item
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"DJCollectionViewCell" forIndexPath:indexPath];
if(!cell){
cell = [[UICollectionViewCell alloc] init];
}
CGFloat red = arc4random()%256/255.0;
CGFloat green = arc4random()%256/255.0;
CGFloat blue = arc4random()%256/255.0;
cell.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];
return cell;
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerView" forIndexPath:indexPath];
UILabel *view = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 40)];
view.backgroundColor = [UIColor orangeColor];
view.text = @"headerView";
[headerView addSubview:view];
return headerView;
}else{
UICollectionReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footerView" forIndexPath:indexPath];
UILabel *view = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 40)];
view.backgroundColor = [UIColor greenColor];
view.text = @"footerView";
[footerView addSubview:view];
return footerView;
}
}
#pragma mark - 点击 某个Item时 调用
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
//取消选中
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
}
-(UICollectionView *)collrctionView{
if (!_collrctionView) {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
// 设置item的行间距和列间距
layout.minimumInteritemSpacing = 15;
layout.minimumLineSpacing = 15;
// 设置item的大小
CGFloat itemW = ([UIScreen mainScreen].bounds.size.width - 65) /4 ;
layout.itemSize = CGSizeMake(itemW, itemW);
layout.headerReferenceSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 40);
layout.footerReferenceSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 40);
// 设置每个分区的 上左下右 的内边距
layout.sectionInset = UIEdgeInsetsMake(10, 10 ,10, 10);
// // 设置区头和区尾的大小
// layout.headerReferenceSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 10);
// layout.footerReferenceSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 10);
// // 设置分区的头视图和尾视图 是否始终固定在屏幕上边和下边
// layout.sectionFootersPinToVisibleBounds = YES;
// 设置滚动条方向
// layout.scrollDirection = UICollectionViewScrollDirectionVertical;
_collrctionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height-100) collectionViewLayout:layout];
_collrctionView.backgroundColor = [UIColor whiteColor];
// _collrctionView.showsVerticalScrollIndicator = NO;
_collrctionView.scrollEnabled = YES;
//注册cell
[_collrctionView registerClass:[DJCollectionViewCell class] forCellWithReuseIdentifier:@"DJCollectionViewCell"];
[_collrctionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footerView"];
[_collrctionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerView"];
_collrctionView.delegate = self;
_collrctionView.dataSource = self;
}
return _collrctionView;
}
@end
2.瀑布流
瀑布流的特点:
(1)瀑布流的列数是固定的,不会动态改变。
(2)每个Item的高都不是固定的,是由Item的内容决定的。
(3)布局时Item总是加到高度比较小的那一列上。
DJCollectionWaterfallLayout的头文件,继承UICollectionViewLayout:
//
// DJCollectionWaterfallLayout.h
// DJTestDemo
//
// Created by admin on 2021/9/14.
//
#import <UIKit/UIKit.h>
extern NSString *const kSupplementaryViewKindHeader;
@protocol DJCollectionWaterfallLayoutProtocol;
@interface DJCollectionWaterfallLayout : UICollectionViewLayout
@property (nonatomic, weak) id<DJCollectionWaterfallLayoutProtocol> delegate;
//行数
@property (nonatomic, assign) NSUInteger columns;
//列间距
@property (nonatomic, assign) CGFloat columnSpacing;
//行间距
@property (nonatomic, assign) CGFloat itemSpacing;
//section到collectionView的边距
@property (nonatomic, assign) UIEdgeInsets insets;
@end
@protocol DJCollectionWaterfallLayoutProtocol <NSObject>
- (CGFloat)collectionViewLayout:(DJCollectionWaterfallLayout *)layout heightForItemAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)collectionViewLayout:(DJCollectionWaterfallLayout *)layout heightForSupplementaryViewAtIndexPath:(NSIndexPath *)indexPath;
@end
//
// DJCollectionWaterfallLayout.m
// DJTestDemo
//
// Created by admin on 2021/9/14.
//
#import "DJCollectionWaterfallLayout.h"
NSString *const kSupplementaryViewKindHeader = @"Header";
CGFloat const kSupplementaryViewKindHeaderPinnedHeight = 44.f;
@interface DJCollectionWaterfallLayout()
/** 保存所有Item的LayoutAttributes */
@property (nonatomic, strong) NSMutableArray<UICollectionViewLayoutAttributes *> *attributesArray;
/** 保存所有列的当前高度 */
@property (nonatomic, strong) NSMutableArray<NSNumber *> *columnHeights;
@end
@implementation DJCollectionWaterfallLayout
- (void)dealloc
{
NSLog(@"%s", __func__);
}
- (instancetype)init
{
if(self = [super init]) {
_columns = 1;
_columnSpacing = 10;
_itemSpacing = 10;
_insets = UIEdgeInsetsZero;
}
return self;
}
#pragma mark - UICollectionViewLayout (UISubclassingHooks)
/**
* 1、
* collectionView初次显示或者调用invalidateLayout方法后会调用此方法
* 触发此方法会重新计算布局,每次布局也是从此方法开始
* 在此方法中需要做的事情是准备后续计算所需的东西,以得出后面的ContentSize和每个item的layoutAttributes
*/
- (void)prepareLayout
{
[super prepareLayout];
//初始化数组
self.columnHeights = [NSMutableArray array];
for(NSInteger column=0; column<_columns; column++){
self.columnHeights[column] = @(0);
}
self.attributesArray = [NSMutableArray array];
NSInteger numSections = [self.collectionView numberOfSections];
for(NSInteger section=0; section<numSections; section++){
NSInteger numItems = [self.collectionView numberOfItemsInSection:0];
for(NSInteger item=0; item<numItems; item++){
//遍历每一项
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
//计算LayoutAttributes
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attributesArray addObject:attributes];
}
}
}
/**
* 2、
* 需要返回所有内容的滚动长度
*/
- (CGSize)collectionViewContentSize
{
NSInteger mostColumn = [self columnOfMostHeight];
//所有列当中最大的高度
CGFloat mostHeight = [self.columnHeights[mostColumn] floatValue];
return CGSizeMake(self.collectionView.bounds.size.width, mostHeight+_insets.top+_insets.bottom);
}
/**
* 3、
* 当CollectionView开始刷新后,会调用此方法并传递rect参数(即当前可视区域)
* 我们需要利用rect参数判断出在当前可视区域中有哪几个indexPath会被显示(无视rect而全部计算将会带来不好的性能)
* 最后计算相关indexPath的layoutAttributes,加入数组中并返回
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributesArray = self.attributesArray;
NSArray<NSIndexPath *> *indexPaths;
//1、计算rect中出现的items
indexPaths = [self indexPathForItemsInRect:rect];
for(NSIndexPath *indexPath in indexPaths){
//计算对应的LayoutAttributes
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[attributesArray addObject:attributes];
}
//2、计算rect中出现的SupplementaryViews
//这里只计算了kSupplementaryViewKindHeader
indexPaths = [self indexPathForSupplementaryViewsOfKind:kSupplementaryViewKindHeader InRect:rect];
for(NSIndexPath *indexPath in indexPaths){
//计算对应的LayoutAttributes
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:kSupplementaryViewKindHeader atIndexPath:indexPath];
[attributesArray addObject:attributes];
}
return attributesArray;
}
/**
* 每当offset改变时,是否需要重新布局,newBounds为offset改变后的rect
* 瀑布流中不需要,因为滑动时,cell的布局不会随offset而改变
* 如果需要实现悬浮Header,需要改为YES
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
//return [super shouldInvalidateLayoutForBoundsChange:newBounds];
return YES;
}
#pragma mark - 计算单个indexPath的LayoutAttributes
/**
* 根据indexPath,计算对应的LayoutAttributes
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//外部返回Item高度
CGFloat itemHeight = [self.delegate collectionViewLayout:self heightForItemAtIndexPath:indexPath];
//headerView高度
CGFloat headerHeight = [self.delegate collectionViewLayout:self heightForSupplementaryViewAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
//找出所有列中高度最小的
NSInteger columnIndex = [self columnOfLessHeight];
CGFloat lessHeight = [self.columnHeights[columnIndex] floatValue];
//计算LayoutAttributes
CGFloat width = (self.collectionView.bounds.size.width-(_insets.left+_insets.right)-_columnSpacing*(_columns-1)) / _columns;
CGFloat height = itemHeight;
CGFloat x = _insets.left+(width+_columnSpacing)*columnIndex;
CGFloat y = lessHeight==0 ? headerHeight+_insets.top : lessHeight+_itemSpacing;
attributes.frame = CGRectMake(x, y, width, height);
//更新列高度
self.columnHeights[columnIndex] = @(y+height);
return attributes;
}
/**
* 根据kind、indexPath,计算对应的LayoutAttributes
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
//计算LayoutAttributes
if([elementKind isEqualToString:kSupplementaryViewKindHeader]){
CGFloat width = self.collectionView.bounds.size.width;
CGFloat height = [self.delegate collectionViewLayout:self heightForSupplementaryViewAtIndexPath:indexPath];
CGFloat x = 0;
//根据offset计算kSupplementaryViewKindHeader的y
//y = offset.y-(header高度-固定高度)
CGFloat offsetY = self.collectionView.contentOffset.y;
CGFloat y = MAX(0,
offsetY-(height-kSupplementaryViewKindHeaderPinnedHeight));
attributes.frame = CGRectMake(x, y, width, height);
attributes.zIndex = 1024;
}
return attributes;
}
#pragma mark - helpers
/**
* 找到高度最小的那一列的下标
*/
- (NSInteger)columnOfLessHeight
{
if(self.columnHeights.count == 0 || self.columnHeights.count == 1){
return 0;
}
__block NSInteger leastIndex = 0;
[self.columnHeights enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
if([number floatValue] < [self.columnHeights[leastIndex] floatValue]){
leastIndex = idx;
}
}];
return leastIndex;
}
/**
* 找到高度最大的那一列的下标
*/
- (NSInteger)columnOfMostHeight
{
if(self.columnHeights.count == 0 || self.columnHeights.count == 1){
return 0;
}
__block NSInteger mostIndex = 0;
[self.columnHeights enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
if([number floatValue] > [self.columnHeights[mostIndex] floatValue]){
mostIndex = idx;
}
}];
return mostIndex;
}
#pragma mark - 根据rect返回应该出现的Items
/**
* 计算目标rect中含有的item
*/
- (NSMutableArray<NSIndexPath *> *)indexPathForItemsInRect:(CGRect)rect
{
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
return indexPaths;
}
/**
* 计算目标rect中含有的某类SupplementaryView
*/
- (NSMutableArray<NSIndexPath *> *)indexPathForSupplementaryViewsOfKind:(NSString *)kind InRect:(CGRect)rect
{
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
if([kind isEqualToString:kSupplementaryViewKindHeader]){
//在这个瀑布流自定义布局中,只有一个位于列表顶部的SupplementaryView
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
//如果当前区域可以看到SupplementaryView,则返回
//CGFloat height = [self.delegate collectionViewLayout:self heightForSupplementaryViewAtIndexPath:indexPath];
//if(CGRectGetMinY(rect) <= height + _insets.top){
//Header默认总是需要显示
[indexPaths addObject:indexPath];
//}
}
return indexPaths;
}
@end
//
// ViewController.m
// DJTestDemo
//
// Created by admin on 2021/6/8.
//
#import "ViewController.h"
#import "DJCollectionWaterfallLayout.h"
@interface ViewController ()<UICollectionViewDelegate,UICollectionViewDataSource,DJCollectionWaterfallLayoutProtocol>
@property(nonatomic,strong)UICollectionView *collrctionView;
@property (nonatomic, strong) NSMutableArray *dataList;
@property (nonatomic, strong) DJCollectionWaterfallLayout *waterfallLayout;
@end
@implementation ViewController
-(void)viewDidLoad{
[super viewDidLoad];
[self setupDataList];
[self.view addSubview:self.collrctionView];
}
#pragma mark - 数据源
- (void)setupDataList{
_dataList = [NSMutableArray array];
NSInteger dataCount = arc4random()%25+50;
for(NSInteger i=0; i<dataCount; i++){
NSInteger rowHeight = arc4random()%100+200;
[_dataList addObject:@(rowHeight)];
}
}
#pragma mark - UICollectionViewDelegate
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 2;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _dataList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//创建item 从缓存池中拿 Item
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"DJCollectionViewCell" forIndexPath:indexPath];
if(!cell){
cell = [[UICollectionViewCell alloc] init];
}
CGFloat red = arc4random()%256/255.0;
CGFloat green = arc4random()%256/255.0;
CGFloat blue = arc4random()%256/255.0;
cell.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];
return cell;
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerView" forIndexPath:indexPath];
UILabel *view = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 40)];
view.backgroundColor = [UIColor orangeColor];
view.text = @"headerView";
[headerView addSubview:view];
return headerView;
}else{
UICollectionReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footerView" forIndexPath:indexPath];
UILabel *view = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 40)];
view.backgroundColor = [UIColor greenColor];
view.text = @"footerView";
[footerView addSubview:view];
return footerView;
}
}
#pragma mark - CollectionWaterfallLayoutProtocol
- (CGFloat)collectionViewLayout:(DJCollectionWaterfallLayout *)layout heightForItemAtIndexPath:(NSIndexPath *)indexPath{
NSInteger row = indexPath.row;
CGFloat cellHeight = [_dataList[row] floatValue];
return cellHeight;
}
- (CGFloat)collectionViewLayout:(DJCollectionWaterfallLayout *)layout heightForSupplementaryViewAtIndexPath:(NSIndexPath *)indexPath{
return 0;
}
#pragma mark - 点击 某个Item时 调用
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
//取消选中
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
}
-(UICollectionView *)collrctionView{
if (!_collrctionView) {
_waterfallLayout = [[DJCollectionWaterfallLayout alloc] init];
_waterfallLayout.delegate = self;
_waterfallLayout.columns = 4;
_waterfallLayout.columnSpacing = 10;
_waterfallLayout.insets = UIEdgeInsetsMake(10, 10, 10, 10);
_collrctionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height-100) collectionViewLayout:_waterfallLayout];
_collrctionView.backgroundColor = [UIColor whiteColor];
// _collrctionView.showsVerticalScrollIndicator = NO;
_collrctionView.scrollEnabled = YES;
//注册cell
[_collrctionView registerClass:[DJCollectionViewCell class] forCellWithReuseIdentifier:@"DJCollectionViewCell"];
[_collrctionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footerView"];
[_collrctionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerView"];
_collrctionView.delegate = self;
_collrctionView.dataSource = self;
}
return _collrctionView;
}
@end
3. UICollectionViewLayout
一旦UICollectionView需要刷新(放到屏幕上或需要reloadData
)或者被标记为需要重新计算布局(调用了layout对象的invalidateLayout
方法)时,UICollectionView就会向布局对象请求一系列的方法:
(1)首先会调用prepareLayout
方法,在此方法中尽可能将后续布局时需要用到的前置计算处理好,每次重新布局都是从此方法开始。
(2)调用collectionViewContentSize
方法,根据第一点中的计算来返回所有内容的滚动区域大小。
(3)调用layoutAttributesForElementsInRect:
方法,计算rect内相应的布局,并返回一个装有UICollectionViewLayoutAttributes
的数组,Attributes 跟所有Item一一对应,UICollectionView就是根据这个Attributes来对Item进行布局,并当新的Rect区域滚动进入屏幕时再次请求此方法。
(4)在layoutAttributesForElementsInRect:
方法中,可以单独访问-layoutAttributesForItemAtIndexPath:
方法,来根据indexPath来请求layoutAttributes,虽然这一步不是必须的。
网友评论