自定义UICollectionView布局实现Masonry Layout

集合视图(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;最后,也要像表视图一样指定delegatedataSource

UICollectionViewFlowLayout

若使用流式布局,还需要实现UICollectionViewDelegateFlowLayout协议来调整每个UICollectionViewCell的大小:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

做完这些工作后,可以看到的大致效果如下:

自定义UICollectionView布局实现Masonry Layout_第1张图片
1.png

这是每个item的size都一样的情况下的效果,但是如果每个item的宽度一样,高度却不一样会如何?答案是:
自定义UICollectionView布局实现Masonry Layout_第2张图片
2.png

因为如果使用 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;
}

效果如下:


自定义UICollectionView布局实现Masonry Layout_第3张图片
3.png

你可能感兴趣的:(自定义UICollectionView布局实现Masonry Layout)