美文网首页
iOS UICollectionView

iOS UICollectionView

作者: 搬砖的crystal | 来源:发表于2021-09-14 15:56 被阅读0次

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,虽然这一步不是必须的。

相关文章

网友评论

      本文标题:iOS UICollectionView

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