集合视图(UICollectionView
)的功能非常强大,它与表视图(UITableView
)非常相似,不同之处在于集合视图本身并不知道自己应该怎样布局,它将布局方式委托给了UICollectionLayout的子类。系统本身提供了一个强大的子类——流式布局(UICollectionViewFlowLayout
),可以通过设置scrollDirection
属性来选择集合视图是水平滚动还是竖直滚动,也可以设置每个UICollectionViewCell
之间的间隔;这个类通过UICollectionViewDelegateFlowLayout
协议调整每个UICollectionViewCell
的大小。
添加UICollectionview
添加集合视图
使用代码添加集合视图,需要在init方法中选择布局方式,具体方法是:- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout;
除此之外,还需要指定集合视图的cell类和cell的重用标识,具体方法是:- (void)registerClass:(Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
最后,也要像表视图一样指定delegate
和dataSource
。
UICollectionViewFlowLayout
若使用流式布局,还需要实现UICollectionViewDelegateFlowLayout
协议来调整每个UICollectionViewCell
的大小:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
做完这些工作后,可以看到的大致效果如下:
这是每个item的size都一样的情况下的效果,但是如果每个item的宽度一样,高度却不一样会如何?答案是:
因为如果使用
UICollectionViewFlowLayout
,该布局会先计算一行中所有item的最大高度,然后开始布局下一行的item,这样做就会使每个item都会占据这一行的最大高度,所以导致了这些空白。解决方案就是自己自定义
UICollectionViewLayout
。
Masonry Layout
要实现MasonryLayout
(也称石工布局)需要自定义UICollectionViewLayout
。首先建立UICollectionViewLayout
的子类并定义一个得到目标位置item大小的协议方法,最后覆盖UICollectionViewLayout
的三个方法:
- (void)prepareLayout;
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
- (CGSize)collectionViewContentSize;
prepareLayout
方法会在集合视图开始布局前被调用,在这个方法中,需要计算item的布局方式;
layoutAttributesForElementsInRect:
方法则需要返回在rect以内的item的布局方式;
collectionViewContentSize
方法则需要返回当前集合视图的contentSize
具体例子如下:
//MasonryLayout.h
#import
#define MasonryCollectionViewSpaceWidth 10
typedef NS_ENUM(NSInteger, LayoutStyle) {
LayoutStyleInOrder = 0, //顺序排列cell
LayoutStyleRegular = 1, //整齐排列cell
};
@protocol MasonryLayoutDelegate;
@interface MasonryLayout : UICollectionViewLayout
@property (nonatomic, weak) id delegate;
- (instancetype)initWithLayoutStyle:(LayoutStyle)style;
@end
@protocol MasonryLayoutDelegate
//返回indexPath位置cell的高度
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(MasonryLayout *)layout heightForItemAtIndexPath:(NSIndexPath *)indexPath;
@end
//MasonryLayout.m
#import "MasonryLayout.h"
@interface MasonryLayout ()
{
NSUInteger _numberOfColumns; //列数
NSMutableDictionary* _layoutInfo; //储存每个cell的UICollectionViewLayoutAttributes
NSMutableDictionary* _lastYValueForColumn; //储存每一列当前最大y坐标
LayoutStyle _style;
}
@end
@implementation MasonryLayout
- (instancetype)initWithLayoutStyle:(LayoutStyle)style
{
self = [super init];
if (self) {
_style = style;
}
return self;
}
- (void)prepareLayout
{
_numberOfColumns = 2; //有两列cell
_lastYValueForColumn = [NSMutableDictionary dictionary];
_layoutInfo = [NSMutableDictionary dictionary];
switch (_style) {
case LayoutStyleInOrder:{
[self getLayoutInfoInOrder];
}
break;
case LayoutStyleRegular:{
[self getLayoutInfoRegular];
}
break;
default:
break;
}
}
- (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 function
- (void)getLayoutInfoInOrder
{
NSUInteger currentColumn = 0;
CGFloat itemWidth = ([UIScreen mainScreen].bounds.size.width - MasonryCollectionViewSpaceWidth * (_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 = MasonryCollectionViewSpaceWidth + (itemWidth + MasonryCollectionViewSpaceWidth) * currentColumn;
CGFloat originY = [_lastYValueForColumn[@(currentColumn)] doubleValue];
if (originY == 0.0) {
originY = MasonryCollectionViewSpaceWidth;
}
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 + MasonryCollectionViewSpaceWidth);
currentColumn++;
if (currentColumn == _numberOfColumns) {
currentColumn = 0;
}
}
}
}
- (void)getLayoutInfoRegular
{
NSUInteger currentColumn = 0;
CGFloat itemWidth = ([UIScreen mainScreen].bounds.size.width - MasonryCollectionViewSpaceWidth * (_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 = MasonryCollectionViewSpaceWidth + (itemWidth + MasonryCollectionViewSpaceWidth) * currentColumn;
CGFloat originY = [_lastYValueForColumn[@(currentColumn)] doubleValue];
if (originY == 0.0) {
originY = MasonryCollectionViewSpaceWidth;
}
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 + MasonryCollectionViewSpaceWidth);
}
}
}
- (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;
}
@end
//ViewController.m
#import "ViewController.h"
#import "MasonryLayout.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.\
MasonryLayout *layout = [MasonryLayout new];
layout.delegate = self;
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
[collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
collectionView.backgroundColor = [UIColor whiteColor];
collectionView.delegate = self;
collectionView.dataSource = self;
[self.view addSubview:collectionView];
}
#pragma mark -- MasonryLayoutDelegate
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(MasonryLayout *)layout heightForItemAtIndexPath:(NSIndexPath *)indexPath
{
int x = arc4random() % 150 + 50; //生成50-200的随机数
return x;
}
#pragma mark -- UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 50;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
return cell;
}
效果如下: