如果要实现这样的布局,使用UICollectionView无疑是最好的一个选择,在图片数量很大的情况下,collectionView的cell有重用的机制。通过重写collectionView的布局可以实现各种五花八门的布局。
一、实现一个简单的UICollectionView
实现一个简单的UICollectionView和UITableView非常的类似,同样是datasource和delegate的设计模式,datasource为View提供数据源,告诉View 显示什么内容及如何显示他们,delegate提供一些样式的细节以及和用户的交互。
UICollectionViewDataSource
- section的数量(组数量) -numberOfSectionsInCollection:
- 某个section里有多少个item(某一组里面有多少个item) -collectionView:numberOfItemsInSection:
- 对于某个位置应该显示什么样的cell -collectionView:cellForItemAtIndexPath:
实现上面的三个数据源方法,基本就可以保证collectionView正常工作。
关于重用
View的渲染是消耗性能的,为了提高效率,所以必须重用item。与tableView一样在tableView向数据源请求数据之前可以使用
- registerClass:forCellWithReuseIdentifier:
- registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
- registerNib:forCellWithReuseIdentifier:
- registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
这些方法注册cell,这样可以省下每次判断并初始化cell的代码,要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。
UICollectionViewDelegate
数据无关的view的外形啊,用户交互啊什么的,由UICollectionViewDelegate来负责:
- cell的高亮
- cell的选中状态
- 可以支持长按后的菜单
每个cell有独立的高亮事件和选中事件的delegate,用户点击cell的时候,现在会按照以下流程向delegate进行询问:
- collectionView:shouldHighlightItemAtIndexPath: 是否应该高亮?
- collectionView:didHighlightItemAtIndexPath: 如果1回答为是,那么高亮
- collectionView:shouldSelectItemAtIndexPath: 无论1结果如何,都询问是否可以被选中? - collectionView:didUnhighlightItemAtIndexPath: 如果1回答为是,那么现在取消高亮
- collectionView:didSelectItemAtIndexPath: 如果3回答为是,那么选中cell
对应的高亮和选中状态分别由highlighted和selected两个属性表示。
关于Cell
相对于UITableViewCell来说,UICollectionViewCell比较简单。首先UICollectionViewCell不存在各式各样的默认的style,这主要是由于展示对象的性质决定的,因为UICollectionView所用来展示的对象相比UITableView来说要来得灵活,大部分情况下更偏向于图像而非文字,因此需求将会千奇百怪。因此SDK提供给我们的默认的UICollectionViewCell结构上相对比较简单,由下至上:
- 首先是cell本身作为容器view
- 然后是一个大小自动适应整个cell的backgroundView,用作cell平时的背景
- 再其上是selectedBackgroundView,是cell被选中时的背景
- 最后是一个contentView,自定义内容应被加在这个view上
被选中cell的自动变化,所有的cell中的子view,也包括contentView中的子view,在当cell被选中时,会自动去查找view是否有被选中状态下的改变。比如在contentView里加了一个normal和selected指定了不同图片的imageView,那么选中这个cell的同时这张图片也会从normal变成selected,而不需要额外的任何代码。
UICollectionViewLayout
是UICollectionView的精髓…这也是UICollectionView和UITableView最大的不同。UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性,包括但不限于:
位置
尺寸
透明度
层级关系
形状
等等等等…
Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。Apple为我们提供了一个最简单可能也是最常用的默认layout对象,UICollectionViewFlowLayout。Flow Layout简单说是一个直线对齐的layout(流水布局),最常见的Grid View形式即为一种Flow Layout配置。上面的照片架界面就是一个典型的Flow Layout。首先一个重要的属性是itemSize,它定义了每一个item的大小。通过设定itemSize可以全局地改变所有cell的尺寸,如果想要对某个cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
间隔 可以指定item之间的间隔和每一行之间的间隔,和size类似,有全局属性,也可以对每一个item和每一个section做出设定:
@property (CGSize) minimumInteritemSpacing
@property (CGSize) minimumLineSpacing
- -collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
- -collectionView:layout:minimumLineSpacingForSectionAtIndex:
- 滚动方向 由属性scrollDirection确定scroll view的方向,将影响Flow Layout的基本方向和由header及footer确定的section之间的宽度
UICollectionViewScrollDirectionVertical
UICollectionViewScrollDirectionHorizontal
Header和Footer尺寸 同样地分为全局和部分。需要注意根据滚动方向不同,header和footer的高和宽中只有一个会起作用。垂直滚动时section间宽度为该尺寸的高,而水平滚动时为宽度起作用,如图。
@property (CGSize) headerReferenceSize
@property (CGSize) footerReferenceSize
--collectionView:layout:referenceSizeForHeaderInSection:
--collectionView:layout:referenceSizeForFooterInSection:
缩进
@property UIEdgeInsets sectionInset;
--collectionView:layout:insetForSectionAtIndex:
总结
一个UICollectionView的实现包括两个必要部分:UICollectionViewDataSource和UICollectionViewLayout,和一个交互部分:UICollectionViewDelegate。而Apple给出的UICollectionViewFlowLayout已经是一个很强力的layout方案了。
UICollectionViewLayoutAttributes
UICollectionViewLayoutAttributes是一个非常重要的类,先来看看property列表:
//item的frame信息
@property (nonatomic) CGRect frame
//item的中心点信息
@property (nonatomic) CGPoint center
//item的size信息
@property (nonatomic) CGSize size
//item的transfrom信息
@property (nonatomic) CATransform3D transform3D
//item的透明度信息
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
//是否隐藏
@property (nonatomic, getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息,这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。
自定义UICollectionViewLayout
由于Apple已经给出了一种默认的样式UICollectionViewFlowLayout,所以可以继承自UICollectionViewFlowLayout来自定义一种布局,这种方式比较简单,由于继承自UICollectionViewFlowLayout所以collectionView的一些信息父类已经计算好了,只需要调用super就可以了。接着上代码
创建1个空项目在ViewController的.m文件中实现如下代码
#import "ViewController.h"
//线性布局
#import "GZDLineLayout.h"
//自定义的CollectionViewCell
#import "GZDCollectionViewCell.h"
@interface ViewController ()<UICollectionViewDataSource>
//imageName 数组 (图片的命名是按钮1.jpg,2.jpg来命名的)
@property (strong,nonatomic) NSMutableArray * imageNames;
//控制器View里面的CollectionView
@property (weak,nonatomic) UICollectionView *collectionView;
@end
@implementation ViewController
//懒加载图片数组
- (NSMutableArray *)imageNames {
if (!_imageNames) {
_imageNames = [NSMutableArray array];
for (int i = 0; i < 20; i++ ) {
NSString *name = [NSString stringWithFormat:@"%d",i + 1];
[_imageNames addObject:name];
}
}
return _imageNames;
}
//懒加载CollectionView。。。其实没必要
- (UICollectionView *)collectionView {
if (!_collectionView) {
//创建自定义的布局【这个是重点】
GZDLineLayout *layout = [[GZDLineLayout alloc] init];
//创建collectionView并且设置frame ,frame是随便写的
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 200) collectionViewLayout:layout];
//不显示水平滚动条
collectionView.showsHorizontalScrollIndicator = NO;
//不显示垂直滚动条
collectionView.showsVerticalScrollIndicator = NO;
//随便设置了1个背景颜色
collectionView.backgroundColor = [UIColor darkGrayColor];
//设置数据源
collectionView.dataSource = self;
//注册Cell
[collectionView registerClass:[GZDCollectionViewCell class] forCellWithReuseIdentifier:ID];
//添加到控制器的View中
[self.view addSubview:collectionView];
_collectionView = collectionView;
}
return _collectionView;
}
//重用ID
static NSString *const ID = @"cell";
- (void)viewDidLoad {
[super viewDidLoad];
//取出layout
GZDLineLayout *layout =(GZDLineLayout *)self.collectionView.collectionViewLayout;
//为了让第一个Item放在最中间,所以设置了edgeInsets
self.collectionView.contentInset = UIEdgeInsetsMake(0, self.collectionView.bounds.size.width * 0.5 - layout.itemSize.width * 0.5 , 0, self.collectionView.bounds.size.width * 0.5 - layout.itemSize.width * 0.5);
}
//数据源方法返回一共多少个cell
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.imageNames.count;
}
//数据源方法,返回显示的是什么样的cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GZDCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
cell.imageName = self.imageNames[indexPath.item];
return cell;
}
@end
//自定义cell文件
#import "GZDCollectionViewCell.h"
@interface GZDCollectionViewCell()
@property (weak,nonatomic) UIImageView *photoView;
@end
@implementation GZDCollectionViewCell
- (UIImageView *)photoView {
if (!_photoView) {
UIImageView *photoView = [[UIImageView alloc] init];
photoView.layer.borderColor = [[UIColor whiteColor] CGColor];
photoView.layer.borderWidth = 5;
[self.contentView addSubview:photoView];
_photoView = photoView;
}
return _photoView;
}
- (void)setImageName:(NSString *)imageName {
_imageName = [imageName copy];
self.photoView.image = [UIImage imageNamed:_imageName];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.photoView.frame = self.bounds;
}
@end
//重点布局.m文件
#import "GZDLineLayout.h"
@implementation GZDLineLayout
/**
UICollectionViewLayoutAttributes *attrs;
1.一个cell对应一个UICollectionViewLayoutAttributes对象
2.UICollectionViewLayoutAttributes对象决定了cell的frame
这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
* 这个方法的返回值决定了rect范围内所有元素的排布(frame)
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layoutAttrs = [super layoutAttributesForElementsInRect:rect];
//collectionView中心点的位置
CGFloat collectionViewCenterX = self.collectionView.bounds.size.width * 0.5 + self.collectionView.contentOffset.x;
// NSLog(@"%f",self.collectionView.contentOffset.x);
for (UICollectionViewLayoutAttributes *attrs in layoutAttrs) {
//item距离collectionView中点的位置距离
CGFloat delta = ABS(collectionViewCenterX - attrs.center.x);
CGFloat scale = 1 - delta / self.collectionView.bounds.size.width;
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return layoutAttrs;
}
/**
* 当collectionView的显示范围发生改变的时候,是否需要重新刷新布局
* 一旦重新刷新布局,就会重新调用下面的方法:
1.prepareLayout
2.layoutAttributesForElementsInRect:方法
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
/**
重写targetContentOffsetForProposedContentOffset:withScrollingVelocity:方法
作用:返回值决定了collectionView停止滚动时最终的偏移量(contentOffset)
参数:
- proposedContentOffset:原本情况下,collectionView停止滚动时最终的偏移量
- velocity:滚动速率,通过这个参数可以了解滚动的方向
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
//目的的位置,然后计算与中心点的距离 最小的那一个就 = 中心点的位置。
NSArray *layoutAttrs = [self layoutAttributesForElementsInRect:CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height)];
CGFloat centerX = self.collectionView.bounds.size.width * 0.5 + proposedContentOffset.x;
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in layoutAttrs) {
if (!CGRectIntersectsRect(CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.frame.size.width, self.collectionView.frame.size.height), attrs.frame))continue;
CGFloat delta = ABS(attrs.center.x - centerX);
if (delta < ABS(minDelta)) {
minDelta = attrs.center.x - centerX;
}
}
return CGPointMake(proposedContentOffset.x + minDelta, proposedContentOffset.y);
}
/**
* 用来做布局的初始化操作(不建议在init方法中进行布局的初始化操作)
作用:在这个方法中做一些初始化操作
注意:一定要调用[super prepareLayout]
*/
- (void)prepareLayout {
[super prepareLayout];
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(self.collectionView.bounds.size.height * 0.5, self.collectionView.bounds.size.height * 0.5);
}
@end
在初始化一个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来生成更新后的布局。