集合视图(UICollectionView)的功能非常强大,它与表示图(UITableView)非常相似,不同之处在于集合视图本身并不知道自己应该怎样布局,它将布局方式委托给了UICollectionLayout的子类。系统本身提供了一个强大的子类——流式布局(UICollectionViewFlowLayout),可以通过设置scrollDirection属性来选择集合视图是水平滚动还是竖直滚动,也可以设置每个UICollectionViewCell之间的间隔;这个类通过UICollectionViewDelegateFlowLayout协议调整每个UICollectionViewCell的大小。
添加集合视图
使用代码添加集合视图,需要在init方法中选择布局方式,具体方法是:
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout;
除此之外,还需要指定集合视图的cell类和cell的重用标识,具体方法是:
- (void)registerClass:(Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
最后,也要像表视图一样指定delegate和dataSource。做完之后,就可以将集合视图显示到屏幕上了。
流式布局
接下来就要实现集合视图的各种协议方法了,其中,dataSource和delegate的协议方法和表视图的协议方法类似,在这就不多提了,但是,集合视图还需要实现布局方式的协议方法来获取每个item(UICollectionViewCell)的大小,假若使用流式布局,则需要实现:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
这个方法返回对应位置item的size(CGSize),做完这些后,可以看到大致如下的效果:
这是每个item的size都一样的情况下的效果,但是如果每个item的宽度一样,高度却不一样会如何?答案是:
因为如果使用流式布局,该布局会先计算一行中所有item的最大高度,然后开始布局下一行的item,这样做就会使每个item都会占据这一行的最大高度,所以导致了这些空白。解决方案就是自己自定义 UICollectionViewLayout。
Masonry Layout
要实现Masonry Layout(也称石工布局)需要自定义UICollectionViewLayout。首先建立UICollectionViewLayout的子类并定义一个得到目标位置item大小的协议方法,最后覆盖UICollectionViewLayout的三个方法:
- (void)prepareLayout;
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
- (CGSize)collectionViewContentSize;
prepareLayout方法会在集合视图开始布局前被调用,在这个方法中,需要计算item的布局方式;layoutAttributesForElementsInRect:方法则需要返回在rect以内的item的布局方式;collectionViewContentSize方法则需要返回当前集合视图的contentSize。具体例子如下:
//MasonryCollectionViewLayout.h
#import <UIKit/UIKit.h>
#define SpaceWidth 10
@protocol MasonryCollectionViewLayoutDelegate;
@interface MasonryCollectionViewLayout : UICollectionViewLayout
@property (nonatomic, weak) id<MasonryCollectionViewLayoutDelegate> delegate;
@end
@protocol MasonryCollectionViewLayoutDelegate <NSObject>
- (CGFloat) collectionView:(UICollectionView *) collectionView layout:(MasonryCollectionViewLayout *) layout heightForItemAtIndexPath:(NSIndexPath *) indexPath;
@end
//MasonryCollectionViewLayout.m
#import "MasonryCollectionViewLayout.h"
@interface MasonryCollectionViewLayout (){
NSUInteger _numberOfColumns;
NSMutableDictionary* _layoutInfo;
NSMutableDictionary* _lastYValueForColumn;
}
@end
@implementation MasonryCollectionViewLayout
- (void)prepareLayout
{
_numberOfColumns = 2;
_lastYValueForColumn = [NSMutableDictionary dictionary];
_layoutInfo = [NSMutableDictionary dictionary];
//[self getLayoutInfoInOrder];//顺序排列
[self getLayoutInfoRegular];//整齐排列
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray array];
[_layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
return allAttributes;
}
- (CGSize)collectionViewContentSize
{
NSUInteger currentColumns = 0;
CGFloat maxHeight = 0;
do {
CGFloat height = [_lastYValueForColumn[@(currentColumns)] doubleValue];
if (height > maxHeight) {
maxHeight = height;
}
currentColumns ++;
}while (currentColumns < _numberOfColumns);
return CGSizeMake(self.collectionView.frame.size.width, maxHeight);
}
#pragma mark -- private method
- (void)getLayoutInfoInOrder //顺序排列cell
{
NSUInteger currentColumn = 0;
CGFloat itemWidth = ([UIScreen mainScreen].bounds.size.width - SpaceWidth * (_numberOfColumns + 1)) / _numberOfColumns;
NSIndexPath *indexPath;
NSInteger numberOfSection = [self.collectionView numberOfSections];
for (NSInteger section = 0; section < numberOfSection; section++) {
NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < numberOfItem; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat originX = SpaceWidth + (itemWidth + SpaceWidth) * currentColumn;
CGFloat originY = [_lastYValueForColumn[@(currentColumn)] doubleValue];
if (originY == 0.0) {
originY = 12.5;
}
CGFloat itemHeight = [self.delegate collectionView:self.collectionView layout:self heightForItemAtIndexPath:indexPath];
itemAttributes.frame = CGRectMake(originX, originY, itemWidth, itemHeight);
_layoutInfo[indexPath] = itemAttributes;
_lastYValueForColumn[@(currentColumn)] = @(originY + itemHeight + SpaceWidth);
currentColumn++;
if (currentColumn == _numberOfColumns) {
currentColumn = 0;
}
}
}
}
- (void)getLayoutInfoRegular //整齐排列cell
{
NSUInteger currentColumn = 0;
CGFloat itemWidth = ([UIScreen mainScreen].bounds.size.width - SpaceWidth * (_numberOfColumns + 1)) / _numberOfColumns;
NSIndexPath *indexPath;
NSInteger numberOfSection = [self.collectionView numberOfSections];
for (NSInteger section = 0; section < numberOfSection; section++) {
NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < numberOfItem; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
currentColumn = [self getMiniHeightColumn];
CGFloat originX = SpaceWidth + (itemWidth + SpaceWidth) * currentColumn;
CGFloat originY = [_lastYValueForColumn[@(currentColumn)] doubleValue];
if (originY == 0.0) {
originY = 12.5;
}
CGFloat itemHeight = [self.delegate collectionView:self.collectionView layout:self heightForItemAtIndexPath:indexPath];
itemAttributes.frame = CGRectMake(originX, originY, itemWidth, itemHeight);
_layoutInfo[indexPath] = itemAttributes;
_lastYValueForColumn[@(currentColumn)] = @(originY + itemHeight + SpaceWidth);
}
}
}
- (NSUInteger)getMiniHeightColumn
{
NSInteger miniHeightColumn = 0;
CGFloat miniHeight = [_lastYValueForColumn[@(miniHeightColumn)] doubleValue];
for (NSUInteger column = 0; column < _numberOfColumns; column++) {
CGFloat height = [_lastYValueForColumn[@(column)] doubleValue];
if (height < miniHeight) {
miniHeight = height;
miniHeightColumn = column;
}
}
return miniHeightColumn;
}
效果如下: