学习一个东西,要先搞清楚三个问题:UICollectionView 是什么,UICollectionView 能干什么,如何使用 UICollectionView。
如果我们对于这三个问题都能回答的游刃有余了,基本就掌握这个东西了。
UICollectionView 是什么? 首先回顾一下 Collection View 的构成,我们能看到的有三个元素:
Cells
Supplementary Views 追加视图 (类似Header或者Footer)
Decoration Views 装饰视图 (用作背景展示)
在其内部,我们找到了
1 2 3 @property (nonatomic , strong ) UICollectionViewLayout *collectionViewLayout;@property (nonatomic , weak , nullable ) id <UICollectionViewDelegate > delegate;@property (nonatomic , weak , nullable ) id <UICollectionViewDataSource > dataSource;
可见其内部有提供数据的 UICollectionViewDataSource 以及处理用户交互的UICollectionViewDelegate 支持。另一方面,对于cell的样式和组织方式,由于collectionView 比 tableView 要复杂得多,因此没有按照类似于 tableView 的style 的方式来定义,而是专门使用了一个类来对 collectionView 的布局和行为进行描述,这就是 UICollectionViewLayout。collectionView 更像是一个复杂且灵活多变的 tableView。
UICollectionView 能干什么? UICollectionView 可以搭建漂亮的布局页面,比如淘宝展示商品的 app:
这样的页面使用 UITableView 很难实现,但是使用 UICollectionView 是很容易搭建的。你可以自定义布局,让你的控件视图更加灵活。你可以用很少的代码实现非常炫酷的布局效果。
我们如何使用 UICollectionView UICollectionView 的简单使用 先从最基础的开始:
首先,它是一个 UIView 控件,我们要想再屏幕上把它展示出来需要:
1 2 3 4 5 6 CGFloat collectionWH = self .view.frame.size.width;CGRect frame = CGRectMake (0 , 150 , collectionWH, collectionWH); UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame];[self .view addSubview:collectionView];
当我们运行的时候会发现程序崩了,这是由于 UICollectionView 是用来进行屏幕布局的。我不能这样简简单单的创建出来。你会发现 alloc init
方法中,有一个initWithFrame:frame collectionViewLayout:;
方法,我们必须给 collectionView 传入一个 UICollectionViewLayout 对象,这个对象也叫布局对象。
目前来说,苹果官方给我们的能用的布局就一个:UICollectionViewFlowLayout,也叫流水布局。项目中,我们需要自定义布局满足自己项目的需要。
当我们使用下面代码创建 collectionView 时,运行已经可以看到 collectionView 了。如果我们想看到东西的话,还需要设置 collectionView 的数据源方法,这和 UITableView 比较类似。
1 UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
这里就不过多说明了,可以看代码的注释,我们运行下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @interface ViewController ()<UICollectionViewDataSource >@end @implementation ViewController NSString * const AMCellId = @"AMCell" ;- (void )viewDidLoad { [super viewDidLoad]; CGFloat collectionWH = self .view.frame.size.width; CGRect frame = CGRectMake (0 , 150 , collectionWH, collectionWH); UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; [collectionView registerClass:[UICollectionViewCell class ] forCellWithReuseIdentifier:AMCellId]; collectionView.dataSource = self ; [self .view addSubview:collectionView]; } #pragma mark - UICollectionViewDataSource - (NSInteger )collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger )section { return 50 ; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:AMCellId forIndexPath:indexPath]; cell.backgroundColor = [UIColor greenColor]; return cell; }
于是我们已经完成了一次 collectionView 的简单使用。会出现以下效果:
PS:所谓的流水布局,flow 的意思就是当一行装满了就会流到下一行吧,其实就是按顺序排列的九宫格布局。
掌握 UICollectionViewFlowLayout 类的使用 想要自定义布局,我们需要写一个类,这个类必须继承自 UICollectionViewLayout 或者 UICollectionViewFlowLayout。
我们先学习如何通过继承 UICollectionViewFlowLayout 类来做一些与流水效果类似的小例子,便于我们理解 UICollectionViewFlowLayout 是怎样做 collectionView 的布局的。
效果图如下:
首先,创建 AMLineLayout 文件并继承自 UICollectionViewFlowLayout。需要用自定义的布局。然后进行一些初始化操作,比如只允许水平滚动,cell 的 size 等等的初始化操作。
注意:
初始化操作不能像其他类的初始化一样写在 -init,中 UICollectionViewLayout 专门提供了我们初始化方法,而且这个代码一定要调用 super 的方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 - (void )prepareLayout { [super prepareLayout]; self .scrollDirection = UICollectionViewScrollDirectionHorizontal ; self .itemSize = CGSizeMake (100 , 100 ); CGFloat inset = self .collectionView.frame.size.width * 0.5 - self .itemSize.width * 0.5 ; self .sectionInset = UIEdgeInsetsMake (0 , inset, 0 , inset); }
实现滑动远离中心点的 cell 缩小,靠近中心点的 cell 放大,在中心点的 cell 最大。我们需要重写父类方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 - (BOOL )shouldInvalidateLayoutForBoundsChange:(CGRect )newBounds { return YES ; } - (NSArray <UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect )rect { CGFloat centerX = self .collectionView.contentOffset.x + self .collectionView.frame.size.width * 0.5 ; NSArray *array = [super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes *attrs in array) { CGFloat delta = ABS(attrs.center.x - centerX); CGFloat scale = 1 - delta / self .collectionView.frame.size.width; attrs.transform = CGAffineTransformMakeScale (scale, scale); } return array; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - (CGPoint )targetContentOffsetForProposedContentOffset:(CGPoint )proposedContentOffset withScrollingVelocity:(CGPoint )velocity { CGFloat centerX = proposedContentOffset.x + self .collectionView.frame.size.width * 0.5 ; CGRect rect = CGRectMake (proposedContentOffset.x, 0 , self .collectionView.frame.size.width, self .collectionView.frame.size.height); NSArray *array = [super layoutAttributesForElementsInRect:rect]; CGFloat minDelta = MAXFLOAT; for (UICollectionViewLayoutAttributes *attrs in array) { if (ABS(minDelta) > ABS(attrs.center.x - centerX)) { minDelta = attrs.center.x - centerX; } } return CGPointMake (proposedContentOffset.x + minDelta, proposedContentOffset.y); }
此时,你运行程序,就可以发现每个 cell 就像之前的图片那里的样子一样运动了。想要给 cell 添加图片的话,就和我们的 AMLineLayout 类无关了。AMLineLayout 只管布局,不管内容。
此时创建自定义的 UICollectionViewCell 然后设置 imageView 控件添加图片控件,然后在 dataSource 数据源方法中给图片控件赋值并返回我们自定义的 cell 即可,这里就不过多叙述了。具体可见线性布局 。
接下来,如果我们需求的布局不是流水布局,而是其他布局排布,要解决这样的问题,就需要我们自己来实现 UICollectionViewLayout 类中的很多方法了。它是一个抽象类,有很多抽象方法。下面就来详细看看我们应该如何使用它来自定义元素的排布。
掌握 UICollectionViewLayout 类的使用 UICollectionViewLayout 的功能为向 UICollectionView 提供布局信息,不仅包括 cell 的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义 layout 的常规做法是继承 UICollectionViewLayout 类,然后重载下列方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 + (Class)layoutAttributesClass; + (Class)invalidationContextClass NS_AVAILABLE_IOS (7 _0); - (void )prepareLayout; - (nullable NSArray <__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect )rect; - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath; - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; - (BOOL )shouldInvalidateLayoutForBoundsChange:(CGRect )newBounds; - (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect )newBounds NS_AVAILABLE_IOS (7 _0); - (BOOL )shouldInvalidateLayoutForPreferredLayoutAttributes:(UICollectionViewLayoutAttributes *)preferredAttributes withOriginalAttributes:(UICollectionViewLayoutAttributes *)originalAttributes NS_AVAILABLE_IOS (8 _0); - (UICollectionViewLayoutInvalidationContext *)invalidationContextForPreferredLayoutAttributes:(UICollectionViewLayoutAttributes *)preferredAttributes withOriginalAttributes:(UICollectionViewLayoutAttributes *)originalAttributes NS_AVAILABLE_IOS (8 _0); - (CGPoint )targetContentOffsetForProposedContentOffset:(CGPoint )proposedContentOffset withScrollingVelocity:(CGPoint )velocity; - (CGPoint )targetContentOffsetForProposedContentOffset:(CGPoint )proposedContentOffset NS_AVAILABLE_IOS (7 _0); - (CGSize )collectionViewContentSize;
另外需要了解的是,在初始化一个 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
来生成更新后的布局。
UICollectionViewLayoutAttributes UICollectionViewLayoutAttributes是一个非常重要的类,其 property 列表:
1 2 3 4 5 6 7 @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 的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。当 UICollectionView 在获取布局时将针对每一个当前 indexPath 上的元素(包括cell,追加视图和装饰视图),向其 UICollectionViewLayout 实例询问该部件的布局信息,而这个布局信息就由每个元素对应的 UICollectionViewLayoutAttributes 类的实例对象给出。
Demo 瀑布流 自定义 UICollectionViewLayout ,然后重写下列4个方法就可以展示内容了。
详见代码:瀑布流框架
补充 如果我们想要删除、插入或者移动元素,删除、插入或者移动组需要用到 UICollectionView 的方法:
1 2 3 4 5 6 7 8 9 - (void )insertSections:(NSIndexSet *)sections; - (void )deleteSections:(NSIndexSet *)sections; - (void )reloadSections:(NSIndexSet *)sections; - (void )moveSection:(NSInteger )section toSection:(NSInteger )newSection; - (void )insertItemsAtIndexPaths:(NSArray <NSIndexPath *> *)indexPaths; - (void )deleteItemsAtIndexPaths:(NSArray <NSIndexPath *> *)indexPaths; - (void )reloadItemsAtIndexPaths:(NSArray <NSIndexPath *> *)indexPaths; - (void )moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;
如果我们想更换布局,一行代码就可以实现,而且是动态实现:
1 - (void )setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL )animated;
如果我们想在删除、插入或者移动元素时或者之后做一些事情,则需要使用这个方法:
1 - (void )performBatchUpdates:(void (^ __nullable )(void ))updates completion:(void (^ __nullable )(BOOL finished))completion;