多分组多列的不规则布局实现原理(复杂首页布局)

在常见的UI布局中,往往会含有广告栏banner,按钮,商品信息展示等等元素。例如如下的布局格式

其解决方式优先想到的是采用瀑布流自定义布局去实现类似的布局格式

自定义CollectionViewLayout

自定义CollectionViewLayout需要去实现如下的基础方法

1、prepareLayout预先布局方法

2、collectionViewContentSize 返回collectionView的内容尺寸方法

3、layoutAttributesForElementsInRect:(CGRect)rect 返回可见范围内的所有布局

4、layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 返回某个item的布局属性

5、layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath 返回附加视图的布局属性

下面直接上代码去实现类似上图的多分组多列的不规则布局

扫描二维码关注公众号,回复: 2490808 查看本文章

自定义CollectionViewLayout,方法声明文件

#import <UIKit/UIKit.h>
@class CLCollectionViewLayout;
//MARK: 代理方法
@protocol CLCollectionViewLayoutDataSource<NSObject>
@required
/**根据每一个分组返回每个分组中含有多少列数*/
- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout numberOfColumnInSection:(NSInteger)section;
/**返回每一列的高度*/
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout itemHeight:(CGFloat)height heightForItemAtIndexPath:(NSIndexPath *)indexPath;
@optional
/**返回item之间的行间距*/
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
/**返回item之间的列间距*/
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
/**返回每个section附件视图的内容偏移尺寸*/
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout insetForSectionAtIndex:(NSInteger)section;
/**返回header的高度*/
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout referenceHeightForHeaderInSection:(NSInteger)section;
/**返回footer的高度*/
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(CLCollectionViewLayout *)layout referenceHeightForFooterInSection:(NSInteger)section;
@end

@interface CLCollectionViewLayout : UICollectionViewLayout
/*
 @ dataSource 代理属性
 @ minimumLineSpacing 最小行间距
 @ minimumInteritemSpacing 最小列间距
 @ sectionHeadersPinToVisibleBounds sessionHeader是否可滚动,默认可以滚动
 */
@property (nonatomic,weak) id <CLCollectionViewLayoutDataSource> dataSource;
@property (nonatomic,assign) CGFloat minimumLineSpacing;
@property (nonatomic,assign) CGFloat minimumInteritemSpacing;
@property (nonatomic,assign) BOOL sectionHeadersPinToVisibleBounds;
@end

方法实现文件如下

1、创建私有分类

@interface CLCollectionViewLayout()
@property (nonatomic, strong) 
/*属性数组*/
NSMutableArray<NSMutableArray<UICollectionViewLayoutAttributes *> *> *itemLayoutAttributes;
@property (nonatomic, strong) NSMutableArray<UICollectionViewLayoutAttributes *> *headerLayoutAttributes;
@property (nonatomic, strong) NSMutableArray<UICollectionViewLayoutAttributes *> *footerLayoutAttributes;

/*sessionHeader数组*/
@property (nonatomic, strong) NSMutableArray<NSNumber *> *heightOfSections;
@property (nonatomic, assign) CGFloat contentHeight;
@property (strong, nonatomic) NSMutableArray *shouldanimationArr;
@end

2、数据与集合的初始化

-(void)initData {
    self.contentHeight = 0.0;
    self.itemLayoutAttributes = [[NSMutableArray alloc] init];
    self.headerLayoutAttributes = [[NSMutableArray alloc] init];
    self.footerLayoutAttributes = [[NSMutableArray alloc] init];
    self.heightOfSections = [[NSMutableArray alloc] init];
    self.shouldanimationArr = [[NSMutableArray alloc] init];
}

3、重写prepareLayout方法,在重写此方法之前,我们需要对属性数组和变量进行清空布局属性。

- (void)prepareLayout {
    [super prepareLayout];
    /*断言判断dataSource是否实现*/
    NSAssert(self.dataSource != nil, @"CommonLayout.dataSource cann't be nil.");
    if (self.collectionView.isDecelerating || self.collectionView.isDragging) {
        return;
    }
    [self initData];
    
    /*布局准备*/
    UICollectionView *collectionView = self.collectionView;
    NSInteger const numberOfSections = collectionView.numberOfSections;
    UIEdgeInsets const contentInset = collectionView.contentInset;
    CGFloat const contentWidth = collectionView.bounds.size.width - contentInset.left - contentInset.right;
    
    for (NSInteger section=0; section < numberOfSections; section++) {
        NSInteger const columnOfSection = [self.dataSource collectionView:collectionView layout:self numberOfColumnInSection:section];
        NSAssert(columnOfSection > 0, @"[XPCollectionViewWaterfallFlowLayout collectionView:layout:numberOfColumnInSection:] must be greater than 0.");
        UIEdgeInsets const contentInsetOfSection = [self contentInsetForSection:section];
        CGFloat const minimumLineSpacing = [self minimumLineSpacingForSection:section];
        CGFloat const minimumInteritemSpacing = [self minimumInteritemSpacingForSection:section];
        CGFloat const contentWidthOfSection = contentWidth - contentInsetOfSection.left - contentInsetOfSection.right;
        CGFloat const height = (contentWidthOfSection-(columnOfSection-1)*minimumInteritemSpacing) / columnOfSection;
        NSInteger const numberOfItems = [collectionView numberOfItemsInSection:section];
        
        /*头视图的布局*/
        CGFloat headerHeight = 0.0;
        if ([self.dataSource respondsToSelector:@selector(collectionView:layout:referenceHeightForHeaderInSection:)]) {
            headerHeight = [self.dataSource collectionView:collectionView layout:self referenceHeightForHeaderInSection:section];
        }
        UICollectionViewLayoutAttributes *headerLayoutAttribute = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
        headerLayoutAttribute.frame = CGRectMake(0.0, self.contentHeight, contentWidth, headerHeight);
        [self.headerLayoutAttributes addObject:headerLayoutAttribute];
        CGFloat offsetOfColumns[columnOfSection];
        for (NSInteger i=0; i<columnOfSection; i++) {
            offsetOfColumns[i] = headerHeight + contentInsetOfSection.top;
        }
        
        /*item布局*/
        NSMutableArray *layoutAttributeOfSection = [NSMutableArray arrayWithCapacity:numberOfItems];
        for (NSInteger item=0; item<numberOfItems; item++) {
            NSInteger currentColumn = 0;
            for (NSInteger i=1; i<columnOfSection; i++) {
                if (offsetOfColumns[currentColumn] > offsetOfColumns[i]) {
                    currentColumn = i;
                }
            }
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            CGFloat itemHeight = [self.dataSource collectionView:collectionView layout:self itemHeight:height heightForItemAtIndexPath:indexPath];
            CGFloat x = contentInsetOfSection.left + height*currentColumn + minimumInteritemSpacing*currentColumn;
            CGFloat y = offsetOfColumns[currentColumn] + (item>=columnOfSection ? minimumLineSpacing : 0.0);
            UICollectionViewLayoutAttributes *layoutAttbiture = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            layoutAttbiture.frame = CGRectMake(x, y + self.contentHeight, height, itemHeight);
            [layoutAttributeOfSection addObject:layoutAttbiture];
            offsetOfColumns[currentColumn] = (y + itemHeight);
        }
        [self.itemLayoutAttributes addObject:layoutAttributeOfSection];
        
        CGFloat maxOffsetValue = offsetOfColumns[0];
        for (int i=1; i<columnOfSection; i++) {
            if (offsetOfColumns[i] > maxOffsetValue) {
                maxOffsetValue = offsetOfColumns[i];
            }
        }
        maxOffsetValue += contentInsetOfSection.bottom;
        
        /*footer布局*/
        CGFloat footerHeader = 0.0;
        if ([self.dataSource respondsToSelector:@selector(collectionView:layout:referenceHeightForFooterInSection:)]) {
            footerHeader = [self.dataSource collectionView:collectionView layout:self referenceHeightForFooterInSection:section];
        }
        UICollectionViewLayoutAttributes *footerLayoutAttribute = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
        footerLayoutAttribute.frame = CGRectMake(0.0, _contentHeight+maxOffsetValue, contentWidth, headerHeight);
        [self.footerLayoutAttributes addObject:footerLayoutAttribute];
        
        CGFloat currentSectionHeight = maxOffsetValue + footerHeader;
        [self.heightOfSections addObject:@(currentSectionHeight)];
        self.contentHeight += currentSectionHeight;
    }
}

4、返回可见的布局属性数组与item和附加视图布局属性

/*返回可见的布局属性数组*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray array];
    [self.itemLayoutAttributes enumerateObjectsUsingBlock:^(NSMutableArray<UICollectionViewLayoutAttributes *> *layoutAttributeOfSection, NSUInteger idx, BOOL *stop) {
        [layoutAttributeOfSection enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attribute, NSUInteger idx, BOOL *stop) {
            if (CGRectIntersectsRect(rect, attribute.frame)) {
                [result addObject:attribute];
            }
        }];
    }];
    [self.headerLayoutAttributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attribute, NSUInteger idx, BOOL *stop) {
        if (attribute.frame.size.height && CGRectIntersectsRect(rect, attribute.frame)) {
            [result addObject:attribute];
        }
    }];
    [self.footerLayoutAttributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attribute, NSUInteger idx, BOOL *stop) {
        if (attribute.frame.size.height && CGRectIntersectsRect(rect, attribute.frame)) {
            [result addObject:attribute];
        }
    }];
    
    /*头视图是否进行移动*/
    if (self.sectionHeadersPinToVisibleBounds) {
        for (UICollectionViewLayoutAttributes *attriture in result) {
            if (![attriture.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) continue;
            NSInteger section = attriture.indexPath.section;
            UIEdgeInsets contentInsetOfSection = [self contentInsetForSection:section];
            NSIndexPath *firstIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            UICollectionViewLayoutAttributes *itemAttribute = [self layoutAttributesForItemAtIndexPath:firstIndexPath];
            CGFloat headerHeight = CGRectGetHeight(attriture.frame);
            CGRect frame = attriture.frame;
            frame.origin.y = MIN(
                                 MAX(self.collectionView.contentOffset.y, CGRectGetMinY(itemAttribute.frame)-headerHeight-contentInsetOfSection.top),
                                 CGRectGetMinY(itemAttribute.frame)+[_heightOfSections[section] floatValue]
                                 );
            attriture.frame = frame;
            attriture.zIndex = (NSIntegerMax/2)+section;
        }
    }
    
    return result;
}

/*返回item布局属性*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return self.itemLayoutAttributes[indexPath.section][indexPath.item];
}

/*返回header或者footer布局属性*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath {
    if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) {
        return self.headerLayoutAttributes[indexPath.item];
    }
    if ([elementKind isEqualToString:UICollectionElementKindSectionFooter]) {
        return self.footerLayoutAttributes[indexPath.item];
    }
    return nil;
}

/*瀑布流的内容尺寸*/
- (CGSize)collectionViewContentSize {
    UIEdgeInsets contentInset = self.collectionView.contentInset;
    CGFloat width = CGRectGetWidth(self.collectionView.bounds) - contentInset.left - contentInset.right;
    CGFloat height = MAX(CGRectGetHeight(self.collectionView.bounds), self.contentHeight);
    return CGSizeMake(width, height);
}

5、更新item布局时的操作,用于item的移动操作

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    [super prepareForCollectionViewUpdates:updateItems];
    NSMutableArray *indexPaths = [NSMutableArray array];
    for (UICollectionViewUpdateItem *updateItem in updateItems) {
        switch (updateItem.updateAction) {
            case UICollectionUpdateActionInsert:
                [indexPaths addObject:updateItem.indexPathAfterUpdate];
                break;
            case UICollectionUpdateActionDelete:
                [indexPaths addObject:updateItem.indexPathBeforeUpdate];
                break;
            case UICollectionUpdateActionMove:
                [indexPaths addObject:updateItem.indexPathBeforeUpdate];
                [indexPaths addObject:updateItem.indexPathAfterUpdate];
                break;
            default:
                NSLog(@"unhandled case: %@", updateItem);
                break;
        }
    }
    self.shouldanimationArr = indexPaths;
}

6、处理方法

- (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    if ([self.shouldanimationArr containsObject:itemIndexPath]) {
        UICollectionViewLayoutAttributes *attr = self.itemLayoutAttributes[itemIndexPath.section][itemIndexPath.item];
        attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI);
        attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));
        attr.alpha = 1;
        [self.shouldanimationArr removeObject:itemIndexPath];
        return attr;
    }
    return nil;
}

- (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    if ([self.shouldanimationArr containsObject:itemIndexPath]) {
        UICollectionViewLayoutAttributes *attr = self.itemLayoutAttributes[itemIndexPath.section][itemIndexPath.item];
        attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(2, 2), 0);
        //        attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));
        attr.alpha = 0;
        [self.shouldanimationArr removeObject:itemIndexPath];
        return attr;
    }
    return nil;
}

- (void)finalizeCollectionViewUpdates
{
    self.shouldanimationArr = nil;
}

/*视图更新布局操作*/
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    CGRect oldBounds = self.collectionView.bounds;
    if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) {
        return YES;
    }
    return NO;
}

/*私有处理方法*/
- (UIEdgeInsets)contentInsetForSection:(NSInteger)section {
    UIEdgeInsets edgeInsets = UIEdgeInsetsZero;
    if ([self.dataSource respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
        edgeInsets = [self.dataSource collectionView:self.collectionView layout:self insetForSectionAtIndex:section];
    }
    return edgeInsets;
}

- (CGFloat)minimumLineSpacingForSection:(NSInteger)section {
    CGFloat minimumLineSpacing = self.minimumLineSpacing;
    if ([self.dataSource respondsToSelector:@selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:)]) {
        minimumLineSpacing = [self.dataSource collectionView:self.collectionView layout:self minimumLineSpacingForSectionAtIndex:section];
    }
    return minimumLineSpacing;
}

- (CGFloat)minimumInteritemSpacingForSection:(NSInteger)section {
    CGFloat minimumInteritemSpacing = self.minimumInteritemSpacing;
    if ([self.dataSource respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) {
        minimumInteritemSpacing = [self.dataSource collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section];
    }
    return minimumInteritemSpacing;
}

使用自定义布局方法

import UIKit
class CollectionView: UIView ,CLCollectionViewLayoutDataSource, UICollectionViewDataSource{
    //分组的列数
    var columns:Array<Int> = [1,4,2,1,2,1,2]
    // items的高度
    var itemsHeight:Array<Array<CGFloat>> = [[150],[75],[150],[150],[300,150,150],[150],[150,180,180,150]]
    // items的个数
    var itemsCount:Array<Int> = [1,8,4,1,3,1,4]

    lazy var customerLayout:CLCollectionViewLayout = {
        let lay:CLCollectionViewLayout = CLCollectionViewLayout.init()
        lay.dataSource = self
        lay.minimumLineSpacing = 0
        lay.minimumInteritemSpacing = 0
        return lay
    }()
    
    lazy var collectionView:UICollectionView = {
        let collection:UICollectionView = UICollectionView.init(frame: self.frame, collectionViewLayout: self.customerLayout)
        collection.backgroundColor = UIColor.white
        collection.dataSource = self
        collection.register(CLCollectionViewCell.self , forCellWithReuseIdentifier: "cellid")
        return collection
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(self.collectionView)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    /*CLCollectionViewLayoutDataSource*/
    // 每一分组的列数
    func collectionView(_ collectionView: UICollectionView!, layout: CLCollectionViewLayout!, numberOfColumnInSection section: Int) -> Int {
        return self.columns[section]
    }
    // 每一个item的高度
    func collectionView(_ collectionView: UICollectionView!, layout: CLCollectionViewLayout!, itemHeight height: CGFloat, heightForItemAt indexPath: IndexPath!) -> CGFloat {
        let itemHeightArray:Array<CGFloat> = self.itemsHeight[indexPath.section]
        if itemHeightArray.count == 1 {
            return itemHeightArray[0]
        } else {
            return itemHeightArray[indexPath.row]
        }
    }
    
    /*UICollectionViewDataSource*/
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return self.columns.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.itemsCount[section]
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell:CLCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellid", for: indexPath) as! CLCollectionViewCell
        cell.titleLabel.text = "\(indexPath.section,indexPath.row)"
        return cell
    }

自定义的普布流cell

import UIKit

class CLCollectionViewCell: UICollectionViewCell {
    lazy var titleLabel:UILabel = {
        let label:UILabel = UILabel.init(frame: .zero)
        label.layer.borderColor = UIColor.gray.cgColor
        label.layer.borderWidth = 1.5
        label.textAlignment = .center
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.contentView.addSubview(self.titleLabel)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        self.titleLabel.mas_makeConstraints { (make:MASConstraintMaker!) in
            make.left.top().offset()(5)
            make.bottom.right().offset()(-5)
        }
    }
}

运行最终结果如下

猜你喜欢

转载自blog.csdn.net/die_word/article/details/81093941