UICollectionView
UICollectionView和UITableView很类似,不过对于我个人来讲,UITableView是经常用到的东西,UICollectionView使用较少,所以这篇文章讲UICollectionView。
1.类和协议
1).UICollectionViewController:与UITableViewController功能类似
2).UICollectionViewCell:与UITableViewCell功能类似,同样有ReuseIdentifier,所以它也有复用机制。
从storyBoard中出列:
MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];
从nib中注册:
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];
3).UICollectionViewDataSource:数据源协议
4).UICollectionViewDelegate:处理包含选中事件的各种方法的协议
5).UICollectionViewDelegateFlowLayout:这是UICollectionView和UITableView不同的地方,它可以用来定制一些布局。
2.例子
1).初始化
新建一个工程,删除ViewController类,将storyBoard中的ViewController替换为UICollectionViewController。
像往常一样,你的主要内容显示在 cell 中,cell 可以被任意分组到 section 中。Collection view 的 cell 必须是 UICollectionViewCell 的子类。所以我们新建UICollectionViewController与UICollectionViewCell的子类,将storyBoard中UICollectionViewController的custom class设置为MyCollectionViewController,将UICollectionViewCell的custom class设置为MyCollectionViewCell。
在UICollectionViewCell中新增如下图两个控件,UIImageView和UILabel
不要忘记设置cell的Identifier:
建立两个IBOutlet:
2).实现数据源方法
MyCollectionViewController.m:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];
return cell;
}
配置cell,MyCollectionViewCell.m
-(void)awakeFromNib{
[super awakeFromNib];
self.backgroundColor = [UIColor randomColor];
}
现在运行,如下图:
IMG_0939.PNG旋转屏幕后栅格会自动旋转并对齐:
IMG_0940.PNG2).实现委托方法
a.高亮
在cell中添加一个selectedBackgroundView视图:
-(void)awakeFromNib{
[super awakeFromNib];
self.selectedBackgroundView = [[UIView alloc]initWithFrame:self.frame];
self.selectedBackgroundView.backgroundColor = [UIColor blackColor];
self.backgroundColor = [UIColor randomColor];
}
实现以下代理方法:
-(BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
return YES;
}
//放大缩小效果
-(void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
[UIView animateWithDuration:kAnimationDuration animations:^{
selectedCell.transform = CGAffineTransformMakeScale(2.0f, 2.0f);
}];
}
-(void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
[UIView animateWithDuration:kAnimationDuration animations:^{
selectedCell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
}];
}
现在按下collectionCell会显示高亮状态:背景颜色变黑色,且有一个弹跳的放大缩小效果。
b.选中
如上右边新建一个MyDetailsViewController,并且从左边控制器中segue到MyDetailsViewController。
MyDetailsViewController.m
-(IBAction) doneTapped:(id) sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.imageView.image = [UIImage imageNamed:@"image"];
}
实现以下代理方法:
MyCollectionViewController.m
-(BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath{
return YES;
}
-(void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (int64_t)1*NSEC_PER_SEC);
dispatch_after(delayInNanoSeconds, dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:@"MainSegue" sender:indexPath];
});
}
这样在高亮效果1秒后会进行视图切换。
4).添加头部和尾部视图
collection view 额外管理着两种视图:supplementary views , Supplementary views 相当于 table view 的 section header 和 footer views。像 cells 一样,他们的内容都由数据源对象驱动。然而和 table view 中用法不一样,supplementary view 并不一定会作为 header 或 footer view;他们的数量和放置的位置完全由布局控制。
Supplementary views必须是 UICollectionReusableView的子类。布局使用的每个视图类都需要在 collection view 中注册,这样当 data source 让它们从 reuse pool 中出列时,它们才能够创建新的实例。首先我们需要在storyBoard中启用"Section Header"和"Section Footer"
之后XCode会自动生成两个UICollectionResuableView到视图中:
然后同样的你可以设置Identifier,然后在以下代理方法中dequeue即可,确实很分别,相比UITableView又进一步封装。
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath{
NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
return [collectionView dequeueReusableSupplementaryViewOfKind: UICollectionElementKindSectionFooter
withReuseIdentifier:SupplementaryViewIdentifier
forIndexPath:indexPath];
}
在这个demo,我演示下如何通过加载自定义的nib控件来添加头部和尾部视图,如下我们新建两个自定义nib控件:
MyCollectionViewController中加载并注册nib:
-(void)awakeFromNib{
UINib *headerNib = [UINib nibWithNibName:NSStringFromClass([Header class]) bundle:[NSBundle mainBundle]];
[self.collectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kCollectionViewHeaderIndentifier];
UINib *footerNib = [UINib nibWithNibName:NSStringFromClass([Footer class]) bundle:[NSBundle mainBundle]];
[self.collectionView registerNib:footerNib forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:kCollectionViewFooterIndentifier];
}
代理方法类似:
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath{
NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
resueIndentifier = kCollectionViewFooterIndentifier;
}
UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
Header *header = (Header *)view;
header.label.text = [NSString stringWithFormat:@"Section Header %lu",(unsigned long)indexPath.section+1];
}else if ([kind isEqualToString:UICollectionElementKindSectionFooter]){
Footer *footer = (Footer *)view;
NSString *title = [NSString stringWithFormat:@"Section Footer %lu",(unsigned long)indexPath.section+1];
[footer.button setTitle:title forState:UIControlStateNormal];
}
return view;
}
UICollectionView和UITableView最重要的区别就是UICollectionView并不知道如何布局,它把布局机制委托给了UICollectionViewLayout子类,默认的布局方式是UICollectionFlowViewLayout类提供的流式布局(flow layout),也就是上面例子显示的那样子。这个类允许你通过UICollectionDelegateViewFlowLayout协议调整各自属性。
不过你也可以创建自己的布局方式,通过继承UICollectionViewLayout,现在是一个例子。
3.UICollectionViewLayout子类
上面的例子中,我们所有cell的大小都是一样的,那如果我们的cell大小不一样呢?我们需要实现UICollectionViewDelegateFlowLayout的协议方法collectionView:layout:sizeForItemAtIndexPath:,但这会使得效果就像下面左边那张图。它会计算每一排中的最大高度,这样会让效果看起来不怎么样。我们可以继承UICollectionViewLayout来实现右图中的效果。
我们新建一个UICollectionViewController,并把程序运行开始移到改控制器。
像上面的例子那样,显示50个同样大小的单元,具体上面已经介绍了,之后它看起来像这样:
现在实现UICollectionViewDelegateFlowLayout的协议方法随机改变cell大小的高度:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat randomHeight = 80 + (arc4random() % 150);
return CGSizeMake(80, randomHeight);
}
现在效果是这样:
现在创建一个UICollectionViewLayout的子类:CustomCollectionViewLayout.首先我们需要像UICollectionViewDelegateFlowLayout一样通过代理的方式来获取特定indexPath上cell的高度。
@class CustomCollectionViewLayout;
@protocol CustomCollectionViewLayoutDelegate <NSObject>
@required
- (CGFloat) collectionView:(UICollectionView*) collectionView
layout:(CustomCollectionViewLayout*) layout
heightForItemAtIndexPath:(NSIndexPath*) indexPath;
@end
子类需要覆盖父类以下3个方法:
-(void) prepareLayout;
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
-(CGSize) collectionViewContentSize;
prepareLayout在布局开始之前会被调用,我们需要在这个方法中计算边框,所以我们引入numberOfColumns 和 interItemSpacing两个变量。分别是item每行的数目和item间的间距,所以头文件如下:
@interface CustomCollectionViewLayout : UICollectionViewLayout
@property (nonatomic, assign) NSUInteger numberOfColumns;
@property (nonatomic, assign) CGFloat interItemSpacing;
@property (weak, nonatomic) id<CustomCollectionViewLayoutDelegate> delegate;
@end
在开始布局前会执行的方法prepareLayout中,我们需要计算每个item的frame值,并把它存入字典layoutInfo中,然后,在我们覆盖父类的方法layoutAttributesForElementsInRect中,可以返回这个字典中的全部frame总值的数组。
在prepareLayout中,frame的height可以通过代理传入:
CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
collectionView:self.collectionView
layout:self
heightForItemAtIndexPath:indexPath];
frame的width则和numberOfColumns 和 interItemSpacing有关,如下:
//计算Item的宽度
CGFloat fullWidth = self.collectionView.frame.size.width;
CGFloat availableSpaceExcludingPadding = fullWidth - (self.interItemSpacing * (self.numberOfColumns + 1));
CGFloat itemWidth = availableSpaceExcludingPadding / self.numberOfColumns;
x轴和y轴则和当前的indexPath有关,所以我们遍历section和item,得到x轴和y轴,并将之前的高度和宽度加起来得到frame值。
NSIndexPath *indexPath;
NSInteger numSections = [self.collectionView numberOfSections];
//遍历section
for(NSInteger section = 0; section < numSections; section++) {
NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
//遍历item
for(NSInteger item = 0; item < numItems; item++){
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//计算x轴
CGFloat x = self.interItemSpacing + (self.interItemSpacing + itemWidth) * currentColumn;
//计算y轴
CGFloat y = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
//通过协议回传高度值
CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
collectionView:self.collectionView
layout:self
heightForItemAtIndexPath:indexPath];
itemAttributes.frame = CGRectMake(x, y, itemWidth, height);
//下一个item的y轴是当前y轴加上item高度,并且加上间距
y += height;
y += self.interItemSpacing;
//把下一个item的y轴记入到字典中
self.lastYValueForColumn[@(currentColumn)] = @(y);
currentColumn ++;
if(currentColumn == self.numberOfColumns) currentColumn = 0;
//将item的属性记录到字典中
self.layoutInfo[indexPath] = itemAttributes;
}
}
然后在我们需要覆盖的第二个方法中,使用enumerateKeysAndObjectsUsingBlock遍历prepareLayout中的layoutInfo加入一个数组中:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
UICollectionViewLayoutAttributes *attributes,
BOOL *stop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
return allAttributes;
}
最后一个方法是计算collectionView的内容大小,在第一个方法中,我们已经把下每个item的y轴记入到字典lastYValueForColumn中,所以我们通过do-while循环把这个最大的y值给取出来,加上宽度值即可返回collectionView的内容大小。
-(CGSize) collectionViewContentSize {
NSUInteger currentColumn = 0;
CGFloat maxHeight = 0;
do {
//最大高度就是之前字典中的y轴
CGFloat height = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
if(height > maxHeight)
maxHeight = height;
currentColumn ++;
} while (currentColumn < self.numberOfColumns);
return CGSizeMake(self.collectionView.frame.size.width, maxHeight);
}
Done!运行下效果如何:
你可以在这里下载完整的代码。如果你觉得对你有帮助,希望你不吝啬你的star:)