几个自定义的Layout
UICollectionView的强大之处,就在于各种layout的自定义实现,以及它们之间的切换。先看几个相当exiciting的例子吧~
比如,堆叠布局:
圆形布局:
和Cover Flow布局:
所有这些布局都采用了同样的数据源和委托方法,因此完全实现了model和view的解耦。但是如果仅这样,那开源社区也已经有很多相应的解决方案了。Apple的强大和开源社区不能比拟的地方在于对SDK的全局掌控,CollectionView提供了非常简单的API可以令开发者只需要一次简单调用,就可以使用CoreAnimation在不同的layout之间进行动画切换,这种切换必定将大幅增加用户体验,代价只是几十行代码就能完成的布局实现,以及简单的一句API调用,不得不说现在所有的开源代码与之相比,都是相形见拙了…不得不佩服和感谢UIKit团队的努力。
UICollectionViewLayoutAttributes
UICollectionViewLayoutAttributes是一个非常重要的类,先来看看property列表:
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息(在这个层面上说的话,实现一个UICollectionViewLayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。
自定义的UICollectionViewLayout
UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:
// 返回collectionView的内容的尺寸
-(CGSize)collectionViewContentSize
/* 返回rect中的所有的元素的布局属性
* 返回的是包含UICollectionViewLayoutAttributes的NSArray
* UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:
* layoutAttributesForCellWithIndexPath:
* layoutAttributesForSupplementaryViewOfKind:withIndexPath:
* layoutAttributesForDecorationViewOfKind:withIndexPath:
*/
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
// 返回对应于indexPath的位置的cell的布局属性
-(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
// 返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes _)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath
// 返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath
// 当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。
首先,-(void)prepareLayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。
接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。
CircleLayout——完全自定义的Layout,添加删除item,以及手势识别
CircleLayout的例子稍微复杂一些,cell分布在圆周上,点击cell的话会将其从collectionView中移出,点击空白处会加入一个cell,加入和移出都有动画效果。
这放在以前的话估计够写一阵子了,而得益于UICollectionView,基本只需要100来行代码就可以搞定这一切,非常cheap。通过CircleLayout的实现,可以完整地看到自定义的layout的编写流程,非常具有学习和借鉴的意义。
首先,布局准备中定义了一些之后计算所需要用到的参数。
-(void)prepareLayout
{ //和init相似,必须call super的prepareLayout以保证初始化正确
[super prepareLayout];
CGSize size = self.collectionView.frame.size;
_cellCount = [[self collectionView] numberOfItemsInSection:0];
_center = CGPointMake(size.width / 2.0, size.height / 2.0);
_radius = MIN(size.width, size.height) / 2.5;
}
其实对于一个size不变的collectionView来说,除了_cellCount之外的中心和半径的定义也可以扔到init里去做,但是显然在prepareLayout里做的话具有更大的灵活性。因为每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化。
然后,按照UICollectionViewLayout子类的要求,重载了所需要的方法:
//整个collectionView的内容大小就是collectionView的大小(没有滚动)
-(CGSize)collectionViewContentSize
{
return [self collectionView].frame.size;
}
//通过所在的indexPath确定位置。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path]; //生成空白的attributes对象,其中只记录了类型是cell以及对应的位置是indexPath
//配置attributes到圆周上
attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount), _center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
return attributes;
}
//用来在一开始给出一套UICollectionViewLayoutAttributes
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
for (NSInteger i=0 ; i < self.cellCount; i++) {
//这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
现在已经得到了一个circle layout。为了实现cell的添加和删除,需要为collectionView加上手势识别,这个很简单,在ViewController中:
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self.collectionView addGestureRecognizer:tapRecognizer];
对应的处理方法handleTapGesture:为
- (void)handleTapGesture:(UITapGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint]; //获取点击处的cell的indexPath
if (tappedCellPath!=nil) { //点击处没有cell
self.cellCount = self.cellCount - 1;
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:tappedCellPath]];
} completion:nil];
} else {
self.cellCount = self.cellCount + 1;
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]];
} completion:nil];
}
}
}
performBatchUpdates:completion: 再次展示了block的强大的一面..这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:
- initialLayoutAttributesForAppearingItemAtIndexPath:
- initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
- finalLayoutAttributesForDisappearingItemAtIndexPath:
- finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
新的示例demo在Github上也有,链接
在CircleLayout中,实现了cell的动画。
//插入前,cell在圆心位置,全透明
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = CGPointMake(_center.x, _center.y);
return attributes;
}
//删除时,cell在圆心位置,全透明,且只有原来的1/10大
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDeletedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = CGPointMake(_center.x, _center.y);
attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
return attributes;
}
在插入或删除时,将分别以插入前和删除后的attributes和普通状态下的attributes为基准,进行UIView的动画过渡。而这一切并没有很多代码要写,几乎是free的,感谢苹果…
堆叠式布局CustomStackLayout
#import "CustomStackLayout.h"
#define RANDOM_0_1 arc4random_uniform(100)/100.0
/*
由于CustomStackLayout是直接继承自UICollectionViewLayout的,父类没有帮它完成任何的布局,因此,
需要用户自己完全重新对每一个item进行布局,也即设置它们的布局属性UICollectionViewLayoutAttributes
*/
@implementation CustomStackLayout
//重写shouldInvalidateLayoutForBoundsChange,每次重写布局内部都会自动调用
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
//重写collectionViewContentSize,可以让collectionView滚动
-(CGSize)collectionViewContentSize
{
return CGSizeMake(400, 400);
}
//重写layoutAttributesForItemAtIndexPath,返回每一个item的布局属性
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//创建布局实例
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//设置布局属性
attrs.size = CGSizeMake(100, 100);
attrs.center = CGPointMake(self.collectionView.frame.size.width*0.5, self.collectionView.frame.size.height*0.5);
//设置旋转方向
//int direction = (i % 2 ==0)? 1: -1;
NSArray *directions = @[@0.0,@1.0,@(0.05),@(-1.0),@(-0.05)];
//只显示5张
if (indexPath.item >= 5)
{
attrs.hidden = YES;
}
else
{
//开始旋转
attrs.transform = CGAffineTransformMakeRotation([directions[indexPath.item]floatValue]);
//zIndex值越大,图片越在上面
attrs.zIndex = [self.collectionView numberOfItemsInSection:indexPath.section] - indexPath.item;
}
return attrs;
}
//重写layoutAttributesForElementsInRect,设置所有cell的布局属性(包括item、header、footer)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *arrayM = [NSMutableArray array];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
//给每一个item创建并设置布局属性
for (int i = 0; i < count; I++)
{
//创建item的布局属性
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
[arrayM addObject:attrs];
}
return arrayM;
}
@end
效果如图:
圆形布局CustomCircleLayout
#import "CustomCircleLayout.h"
@implementation CustomCircleLayout
//重写shouldInvalidateLayoutForBoundsChange,每次重写布局内部都会自动调用
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
//重写layoutAttributesForItemAtIndexPath,返回每一个item的布局属性
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//创建布局实例
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//设置item的大小
attrs.size = CGSizeMake(50, 50);
//设置圆的半径
CGFloat circleRadius = 70;
//设置圆的中心点
CGPoint circleCenter = CGPointMake(self.collectionView.frame.size.width*0.5, self.collectionView.frame.size.height *0.5);
//计算每一个item之间的角度
CGFloat angleDelta = M_PI *2 /[self.collectionView numberOfItemsInSection:indexPath.section];
//计算当前item的角度
CGFloat angle = indexPath.item * angleDelta;
//计算当前item的中心
CGFloat x = circleCenter.x + cos(angle)*circleRadius;
CGFloat y = circleCenter.y - sin(angle)*circleRadius;
//定位当前item的位置
attrs.center = CGPointMake(x, y);
//设置item的顺序,越后面的显示在前面
attrs.zIndex = indexPath.item;
return attrs;
}
//重写layoutAttributesForElementsInRect,设置所有cell的布局属性(包括item、header、footer)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *arrayM = [NSMutableArray array];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
//给每一个item创建并设置布局属性
for (int i = 0; i < count; I++)
{
//创建item的布局属性
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
[arrayM addObject:attrs];
}
return arrayM;
}
@end
效果图如下:
瀑布流布局
在.h文件里代码结构如下:
#import
/*
为了体现封装性的特点,我们可以把一些数据设置为公共的,既可以提高扩展性和通用性,
也便于外界按照自己的需求做必要的调整。
*/
@protocol WaterFlowLayoutDelegate; //设置代理传递数据,降低了与其他类的耦合性,通用性更强
@class WaterFlowLayout;
@interface WaterFlowLayout : UICollectionViewLayout
@property (assign,nonatomic)CGFloat columnMargin;//每一列item之间的间距
@property (assign,nonatomic)CGFloat rowMargin; //每一行item之间的间距
@property (assign,nonatomic)UIEdgeInsets sectionInset;//设置于collectionView边缘的间距
@property (assign,nonatomic)NSInteger columnCount;//设置每一行排列的个数
@property (weak,nonatomic)id delegate; //设置代理
@end
@protocol WaterFlowLayoutDelegate
-(CGFloat)waterFlowLayout:(WaterFlowLayout *) WaterFlowLayout heightForWidth:(CGFloat)width andIndexPath:(NSIndexPath *)indexPath;
@end
在.m文件里代码结构如下:
#import "WaterFlowLayout.h"
//每一列item之间的间距
//static const CGFloat columnMargin = 10;
//每一行item之间的间距
//static const CGFloat rowMargin = 10;
@interface WaterFlowLayout()
/** 这个字典用来存储每一列item的高度 */
@property (strong,nonatomic)NSMutableDictionary *maxYDic;
/** 存放每一个item的布局属性 */
@property (strong,nonatomic)NSMutableArray *attrsArray;
@end
@implementation WaterFlowLayout
/** 懒加载 */
-(NSMutableDictionary *)maxYDic
{
if (!_maxYDic)
{
_maxYDic = [NSMutableDictionary dictionary];
}
return _maxYDic;
}
/** 懒加载 */
-(NSMutableArray *)attrsArray
{
if (!_attrsArray)
{
_attrsArray = [NSMutableArray array];
}
return _attrsArray;
}
//初始化
-(instancetype)init
{
if (self = [super init]){
self.columnMargin = 10;
self.rowMargin = 10;
self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
self.columnCount = 3;
}
return self;
}
//每一次布局前的准备工作
-(void)prepareLayout
{
[super prepareLayout];
//清空最大的y值
for (int i =0; i < self.columnCount; I++)
{
NSString *column = [NSString stringWithFormat:@"%d",I];
self.maxYDic[column] = @(self.sectionInset.top);
}
//计算所有item的属性
[self.attrsArray removeAllObjects];
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i=0; i [self.maxYDic[maxColumn] floatValue])
{
maxColumn = column;
}
}];
return CGSizeMake(0, [self.maxYDic[maxColumn]floatValue]+self.sectionInset.bottom);
}
//允许每一次重新布局
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
//布局每一个属性
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//假设最短的那一列为第0列
__block NSString *minColumn = @"0";
//遍历字典,找出最短的那一列
[self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSString *column, NSNumber *maxY, BOOL *stop) {
if ([maxY floatValue] < [self.maxYDic[minColumn] floatValue])
{
minColumn = column;
}
}];
//计算每一个item的宽度和高度
CGFloat width = (self.collectionView.frame.size.width - self.columnMargin*(self.columnCount - 1) - self.sectionInset.left - self.sectionInset.right) / self.columnCount;
CGFloat height = [self.delegate waterFlowLayout:self heightForWidth:width andIndexPath:indexPath] ;
//计算每一个item的位置
CGFloat x = self.sectionInset.left + (width + self.columnMargin) * [minColumn floatValue];
CGFloat y = [self.maxYDic[minColumn] floatValue] + self.rowMargin;
//更新这一列的y值
self.maxYDic[minColumn] = @(y + height);
//创建布局属性
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//设置item的frame
attrs.frame = CGRectMake(x, y, width, height);
return attrs;
}
//布局所有item的属性,包括header、footer
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
@end
效果图如下: