一、UIcollectionView介绍
1.1、简介
首先看苹果官方文档 UICollectionView Class Reference 的介绍:
The UICollectionView class manages an ordered collection of data items and presents them using customizable layouts. Collection views provide the same general function as table views except that a collection view is able to support more than just single-column layouts. Collection views support customizable layouts that can be used to implement multi-column grids, tiled layouts, circular layouts, and many more. You can even change the layout of a collection view dynamically if you want.
在WWDC2012中的Introducing Collection Views,苹果首次介绍了UICollectionView,类似UITableView的用法使人很容易接受,但强大的自定义布局,又使其相较于UITableView有了选择它的更多理由,UITableView中的表格只支持单排列表,没有办法支持网格列表模式。
一句话总结就是:UICollectionView与UITableView相似,却提供了可自定义多列网格(Grild)的功能。
下面通过一张图片来了解UICollectionView的构成:
1.2、UICollectionView用来做什么
UICollectionView 类用于管理有序的数据单元,并且可以定制化的显示他们。UICollectionView 除了提供和tableview相同的一般的方法外,还支持一列以上的布局。UICollectionView 支持的定制化布局能够实现网格或是瓷砖的布局效果。你甚至可以动态的改变Collectionview的布局。
二、UICollectionView与UITableView比较
2.1、UICollectionViewDataSource
UICollectionViewDelegate-负责提供展示的数据,实现下面两个必须的委托方法,其实与UITableView并无二意
- numberOfItemsInsection: 某个Section里面有多少item
- cellForItemAtIndexPath: 对于某个位置应该显示什么样的cell,里面会涉及到cell的复用,可参考UITableView
2.2、UICollectionViewDelegate
UICollectionViewDelegate-负责用户的交互、cell的外形,委托方法和UITableView相似,可以选择性实现以下委托方法。
- collectionView:shouleHighlightItemAtIndexPath:是否支持高亮?
- collectionView:didHighlightItemAtIndexPath:如果支持高亮,那么高亮;
- collectionView:shouldSelectItemAtIndexPath:询问是否可以被选中?
- collectionView:didUnhighlightItemAtIndexPath:如果支持高亮,那么现在取消高亮;
- collectionView:didSelectItemAtIndexPath:如果支持可以被选中,那么选中 cell;
2.3、UICollectionViewLayout
在布局上,与UITableView直接使用系统提供的样式,UICollectionView使用的是UICollectionViewLayout来自定义布局样式。
- UICollectionViewLayout是一个抽象基类,需要继承它来为CollectionView生成Layout信息。Layout对象的作用是决定Cells,Supplementary Views和Decoration Views在CollectionView中的布局位置。系统也提供了UICollectionViewFlowLayout-l流水式布局效果
- UICollectionView的显示效果几乎全部由UICollectionViewLayout负责,而真正存储着每一个cell的位置、大小等属性的是UICollectionViewLayoutAttributes。每一个cell对应着一个属于自己的UICollectionViewLayoutAttributes,而UICollectionViewLayout正是利用UICollectionViewLayoutAttributes里存在的信息对每一个cell进行布局。
三、UICollectionView层次结构
UICollectionView类负责管理数据的有序集合以及以自定义布局的模式来呈现这些数据,它提供了一些常用的表格功能,此外还增加了许多单栏布局。UICollectionView支持可以用于实现多列网格、平铺的布局、圆形的布局和更多的自定义布局,甚至可以动态的改变它的布局。
3.1、UICollectionView基本使用
我们要使用UICollectionView得实现UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout这三个协议。UICollectionViewDataSource是它的数据源,UICollectionDelegate是它的呈现样式,UICollectionViewDelegateFlowLayout是它的布局样式。
3.1.1 初始化
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
UICollectionView *colView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
3.1.2 注册UICollectionView使用的cell类型
[colView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"myCell"];
3.1.3 设置代理
colView.delegate = self;
colView.dataSource = self;
3.1.4 实现协议UICollectionViewDataSource
//配置UICollectionView的每个section的item数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [[self loadData] count];
}
//配置section数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
//呈现数据
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = @"myCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
return cell;
}
3.1.5 UICollectionViewDelegateFlowLayout
//配置每个item的size
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(100, 60);
}
//配置item的边距
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(20, 20, 0, 20);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(200, 60);
}
3.1.6 UICollectionViewDelegate
复制代码
//点击item时触发
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"您点击了item:%@", [[self loadData] objectAtIndex:indexPath.row]);
[collectionView cellForItemAtIndexPath:indexPath].backgroundColor = [UIColor redColor];
}
//当前item是否可以点击
- (BOOL) collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (indexPath.row % 2)
{
return YES;
}
return NO;
}
//cell点击时是否高亮,点击cell时的样式和点击后cell的样式
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
}
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.backgroundColor = [UIColor grayColor];
}
3.2、自定义UICollectionViewFlowLayout
使用UICollectionViewFlowLayout之前,我们来了解它内部常用的属性
//同一组当中,垂直方向:行与行之间的间距;水平方向:列与列之间的间距
@property (nonatomic) CGFloat minimumLineSpacing;
//垂直方向:同一行中的cell之间的间距;水平方向:同一列中,cell与cell之间的间距
@property (nonatomic) CGFloat minimumInteritemSpacing;
//每个cell统一尺寸
@property (nonatomic) CGSize itemSize;
//滑动反向,默认滑动方向是垂直方向滑动
@property (nonatomic) UICollectionViewScrollDirection scrollDirection;
//每一组头视图的尺寸。如果是垂直方向滑动,则只有高起作用;如果是水平方向滑动,则只有宽起作用。
@property (nonatomic) CGSize headerReferenceSize;
//每一组尾部视图的尺寸。如果是垂直方向滑动,则只有高起作用;如果是水平方向滑动,则只有宽起作用。
@property (nonatomic) CGSize footerReferenceSize;
//每一组的内容缩进
@property (nonatomic) UIEdgeInsets sectionInset;
3.2.1 NYWaterflowLayout(瀑布流样式)
NYWaterflowLayout继承与UICollectionViewLayout 重写部分方法
核心代码
- (void)prepareLayout
{
[super prepareLayout];
// 清楚以前计算的所有高度
[self.columnHeights removeAllObjects];
for (NSInteger i = 0; i < self.columnCount; i++) {
[self.columnHeights addObject:@(self.edgeInsets.top)];
}
// 清楚之前所有的布局属性
[self.attrsArray removeAllObjects];
// 开始创建每一个cell对应的布局属性
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < count; i++) {
//创建位置
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attributes];
}
}
// 决定cell的排布
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
// 创建布局属性
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// collectionView的宽度
CGFloat collectionViewWidth = self.collectionView.frame.size.width;
// 设置布局属性的frame
CGFloat width = (collectionViewWidth - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
CGFloat h = [self.delegate waterflowLayout:self heightForItemAtIndex:indexPath.item itemWidth:width];
// CGFloat h = 50 + arc4random_uniform(100);
// 找出高度最短的那一列
__block NSInteger destColumn = 0;
__block CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
[self.columnHeights enumerateObjectsUsingBlock:^(NSNumber *columnHeightNumber, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat columnHeight = columnHeightNumber.doubleValue;
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = idx;
}
}];
// 获取最短列的xy
CGFloat x = self.edgeInsets.left + destColumn * (width + self.columnMargin);
CGFloat y = minColumnHeight;
if (y != self.edgeInsets.top) {
y += self.rowMargin;
}
attributes.frame = CGRectMake(x, y, width, h);
//更新最短那列的高度
self.columnHeights[destColumn] = @(CGRectGetMaxY(attributes.frame));
return attributes;
}
// 设置CollectionViewContentSize
- (CGSize)collectionViewContentSize
{
__block CGFloat maxColumnHeight = [self.columnHeights[0] doubleValue];
[self.columnHeights enumerateObjectsUsingBlock:^(NSNumber *columnHeightNumber, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat columnHeight = columnHeightNumber.doubleValue;
if (maxColumnHeight < columnHeight) {
maxColumnHeight = columnHeight;
}
}];
return CGSizeMake(0, maxColumnHeight + self.edgeInsets.bottom);
}
NYWaterflowLayoutDelegate
@required
- (CGFloat)waterflowLayout:(NYWaterflowLayout *)waterflowLayout heightForItemAtIndex:(NSUInteger)index itemWidth:(CGFloat)itemWidth;
@optional
// 瀑布流布局的列数
- (NSInteger)columnCountInWaterflowLayout:(NYWaterflowLayout *)waterflowLayout;
// 列间距
- (CGFloat)columnMarginInWaterflowLayout:(NYWaterflowLayout *)wateflowLayout;
// 行间距
- (CGFloat)rowMarginInWaterflowLayout:(NYWaterflowLayout *)waterflowLayout;
// 边沿间距
- (UIEdgeInsets)edgeInsetsInWaterflowLayout:(NYWaterflowLayout *)waterflowLayout;
效果图
3.2.2 NYCircularLayout(环形布局)
核心代码
- (void)prepareLayout
{
[super prepareLayout];
// 获取item的个数
_itemCount = (int)[self.collectionView numberOfItemsInSection:0];
_attributeArray = [[NSMutableArray alloc] init];
// 先设定大圆的半径取长和宽最短的
CGFloat radius = MIN(self.collectionView.frame.size.width, self.collectionView.frame.size.height)/2;
// 计算圆心的位置
CGPoint center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2);
// 设置每个item的大小为50*50 则半径为25
for (int i = 0; i < _itemCount; i++) {
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
// 设置item的大小
attributes.size = CGSizeMake(50, 50);
// 计算每个item中心的坐标 算出的x,y值还要减去item自身的半径大小
float x = center.x + cosf(2*M_PI/_itemCount * i)*(radius - 25);
float y = center.y + sinf(2*M_PI/_itemCount * i)*(radius - 25);
attributes.center = CGPointMake(x, y);
[_attributeArray addObject:attributes];
}
}
// 设定区域大小
- (CGSize)collectionViewContentSize
{
return self.collectionView.frame.size;
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
return _attributeArray;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
// 创建布局属性
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
return attributes;
}
效果图
3.2.3 CarouselViewLayout(旋转布局)
核心代码
- (void)prepareLayout {
[super prepareLayout];
if (self.visibleCount < 1) {
self.visibleCount = 3;
}
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
_viewHeight = CGRectGetHeight(self.collectionView.frame);
_itemHeight = self.itemSize.height;
self.collectionView.contentInset = UIEdgeInsetsMake((_viewHeight - _itemHeight) / 2, 0, (_viewHeight - _itemHeight) / 2, 0);
} else {
_viewHeight = CGRectGetWidth(self.collectionView.frame);
_itemHeight = self.itemSize.width;
self.collectionView.contentInset = UIEdgeInsetsMake(0, (_viewHeight - _itemHeight) / 2, 0, (_viewHeight - _itemHeight) / 2);
}
}
- (CGSize)collectionViewContentSize {
NSInteger cellCount = [self.collectionView numberOfItemsInSection:0];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
return CGSizeMake(CGRectGetWidth(self.collectionView.frame), cellCount * _itemHeight);
}
return CGSizeMake(cellCount * _itemHeight, CGRectGetHeight(self.collectionView.frame));
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSInteger cellCount = [self.collectionView numberOfItemsInSection:0];
CGFloat centerY = (self.scrollDirection == UICollectionViewScrollDirectionVertical ? self.collectionView.contentOffset.y : self.collectionView.contentOffset.x) + _viewHeight / 2;
NSInteger index = centerY / _itemHeight;
NSLog(@"第几个-----%ld",(long)index);
NSInteger count = (self.visibleCount - 1) / 2;
// 取第一个和当前位置减1的最大值
NSInteger minIndex = MAX(0, (index - count));
// 取最后一个和当前位置加1的最小值
NSInteger maxIndex = MIN((cellCount - 1), (index + count));
NSMutableArray *array = [NSMutableArray array];
//更新可视范围内的布局
for (NSInteger i = minIndex; i <= maxIndex; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[array addObject:attributes];
}
return array;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.size = self.itemSize;
CGFloat cY = (self.scrollDirection == UICollectionViewScrollDirectionVertical ? self.collectionView.contentOffset.y : self.collectionView.contentOffset.x) + _viewHeight / 2;
CGFloat attributesY = _itemHeight * indexPath.row + _itemHeight / 2;
attributes.zIndex = -ABS(attributesY - cY);
CGFloat delta = cY - attributesY;
CGFloat ratio = - delta / (_itemHeight * 2);
CGFloat scale = 1 - ABS(delta) / (_itemHeight * 6.0) * cos(ratio * M_PI_4);
// attributes.transform = CGAffineTransformMakeScale(scale, scale);
CGFloat centerY = attributesY;
switch (self.carouselAnim) {
case HJCarouselAnimRotary:
attributes.transform = CGAffineTransformRotate(attributes.transform, - ratio * M_PI_4);
centerY += sin(ratio * M_PI_2) * _itemHeight / 2;
break;
case HJCarouselAnimCarousel:
centerY = cY + sin(ratio * M_PI_2) * _itemHeight * INTERSPACEPARAM;
break;
case HJCarouselAnimCarousel1:
centerY = cY + sin(ratio * M_PI_2) * _itemHeight * INTERSPACEPARAM;
if (delta > 0 && delta <= _itemHeight / 2) {
attributes.transform = CGAffineTransformIdentity;
CGRect rect = attributes.frame;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
rect.origin.x = CGRectGetWidth(self.collectionView.frame) / 2 - _itemSize.width * scale / 2;
rect.origin.y = centerY - _itemHeight * scale / 2;
rect.size.width = _itemSize.width * scale;
CGFloat param = delta / (_itemHeight / 2);
rect.size.height = _itemHeight * scale * (1 - param) + sin(0.25 * M_PI_2) * _itemHeight * INTERSPACEPARAM * 2 * param;
} else {
rect.origin.x = centerY - _itemHeight * scale / 2;
rect.origin.y = CGRectGetHeight(self.collectionView.frame) / 2 - _itemSize.height * scale / 2;
rect.size.height = _itemSize.height * scale;
CGFloat param = delta / (_itemHeight / 2);
rect.size.width = _itemHeight * scale * (1 - param) + sin(0.25 * M_PI_2) * _itemHeight * INTERSPACEPARAM * 2 * param;
}
attributes.frame = rect;
return attributes;
}
break;
case HJCarouselAnimCoverFlow: {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0/400.0f;
transform = CATransform3DRotate(transform, ratio * M_PI_4, 1, 0, 0);
attributes.transform3D = transform;
}
break;
default:
break;
}
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
attributes.center = CGPointMake(CGRectGetWidth(self.collectionView.frame) / 2, centerY);
} else {
attributes.center = CGPointMake(centerY, CGRectGetHeight(self.collectionView.frame) / 2);
}
return attributes;
}
//当滑动停止时使得item的位置位于中心
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGFloat index = roundf(((self.scrollDirection == UICollectionViewScrollDirectionVertical ? proposedContentOffset.y : proposedContentOffset.x) + _viewHeight / 2 - _itemHeight / 2) / _itemHeight);
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
proposedContentOffset.y = _itemHeight * index + _itemHeight / 2 - _viewHeight / 2;
} else {
proposedContentOffset.x = _itemHeight * index + _itemHeight / 2 - _viewHeight / 2;
}
return proposedContentOffset;
}
效果图
3.2.4 banner轮播器
3.2.4.1 UIScrollView实现
核心代码
//初始化ImageView
- (void)initLayoutSubview
{
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
CGSize size = self.scrollView.bounds.size;
_preImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
[_scrollView addSubview:_preImageView];
_currentImageView = [[UIImageView alloc] initWithFrame:CGRectMake(size.width, 0, size.width, size.height)];
[_scrollView addSubview:_currentImageView];
_nextImageView = [[UIImageView alloc] initWithFrame:CGRectMake(size.width * 2, 0, size.width, size.height)];
[_scrollView addSubview:_nextImageView];
self.scrollView.contentSize = CGSizeMake(3 * self.scrollView.bounds.size.width, 0);
WS(weakSelf);
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(weakSelf.mas_bottom).offset(-10);
make.width.equalTo(weakSelf.mas_width);
make.height.equalTo(@(60));
}];
self.currentPage = 0;
self.pageControl.numberOfPages = self.bannersArray.count;
self.pageControl.currentPage = _currentPage;
[self startTimer];
}
//准备加载页
- (void)pageLoad
{
_preImageView.image = nil;
_currentImageView.image = nil;
_nextImageView.image = nil;
_pageControl.currentPage = _currentPage;
#pragma mark -设置图片
NSString *str = self.bannersArray[_currentPage == 0 ? self.bannersArray.count - 1 : _currentPage - 1];
_preImageView.image = [UIImage imageNamed:str];
str = self.bannersArray[_currentPage];
_currentImageView.image = [UIImage imageNamed:str];
str = self.bannersArray[_currentPage == self.bannersArray.count-1 ? 0 : _currentPage + 1];
_nextImageView.image = [UIImage imageNamed:str];
_scrollView.contentOffset = CGPointMake(self.frame.size.width, 0);
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self scrollViewDidEnd];
[self startTimer];
}
- (void)scrollViewDidEnd
{
int index = _scrollView.contentOffset.x / self.window.bounds.size.width;
if (index == 0) {
_currentPage = _currentPage == 0 ? self.bannersArray.count - 1 : _currentPage - 1;
} else if (index == 2)
{
_currentPage = _currentPage + 1 == self.bannersArray.count ? 0 : _currentPage + 1;
}
[self pageLoad];
}
SDCycleScrollViewhttp://www.oschina.net/p/sdcyclescrollview
效果图
附录
UICollectionView思维导图