瀑布流(Swift版和OC版)

原理分析 :

啥叫瀑布流?就是每次加载出来的cell(或者叫item)都是放到最短的那行,像瀑布流水一样出现,其关键思想就是如何计算出每个将要出现的cell的frame。即x、y、w、h。那咱就从浅入深、顺藤摸瓜的开始分析:
首先最好算的是cell的宽w,显然,根据列数、列与列之间的间距、以及距离屏幕边缘两侧即可算出。即:
w = 屏幕宽度 - 距屏幕两侧的间距 - (列数-1)* 列间距。
第二步:然后再看x。通过分析可以知道,其实只要知道了cell当前的列数,就可算出x。即:
x = 剧屏幕左侧的距离 + 当前列数 * (cell的宽(也就是w)加上列间距)
第三步:紧接着可以分析出,如果知道了某个cell的x,那肯定能知道此cell的y值,因为显然当前cell的y肯定就是当前列的高度(即最大y值),加上行间距即可。
这么分析下来,则重中之重,就是要找到最短的那一列,找到了,一切问题就迎刃而解了。
可以定义一个数组,用来存放列的高度值,再定义第0列是最短的那一列,遍历存放高度的数组,比较得出最小列的列数。然后得到了最小列的列数,则可以通过列的下标计算出x值,进而算出y值。即:
x = 剧屏幕左侧的距离 + 高度最小列的列数 * (cell的宽(也就是w)加上列间距)
y = 高度最小的高度值 + 行间距
算完y值之后,记得更新最短那列的高度值
高度h需要外界来确定,列数也是外界决定,更好运用疯转思想,高内聚、低耦合。

下面直接上轮子:

Swift版

协议

//Swift中定义协议: 必须遵守NSObjectProtocol
@objc protocol WaterfallsLayoutDelegate{
    ///返回每个item的宽高 必须实现
    func waterflowLayout(waterflowLayout:LZYWaterfallsLayout, itemIndex: Int, itemWidth:CGFloat) -> CGFloat

    ///返回列数 (非必须)
   optional func columnCountInWaterflowLayout(waterflowLayout:LZYWaterfallsLayout) -> Int
    
    ///返回列间距 (非必须)
    optional func columnMarginInWaterflowLayout(waterflowLayout:LZYWaterfallsLayout) -> CGFloat
    
    ///返回行间距 (非必须)
    optional func rowMarginInWaterflowLayout(waterflowLayout:LZYWaterfallsLayout) -> CGFloat
    
    ///返回边缘间距 上 左 下 右 (非必须)
    optional func edgeInsetsInWaterflowLayout(waterflowLayout:LZYWaterfallsLayout) -> UIEdgeInsets   
}

自定义的布局类

class LZYWaterfallsLayout: UICollectionViewLayout {
     ///定义一个属性保存代理对象 一定要加上weak, 避免循环引用
    weak var delegate: WaterfallsLayoutDelegate?
    /// 总列数
    var columnCount : Int {
        return delegate?.columnCountInWaterflowLayout?(self) ?? 2
    }
    /// 列间距
    var columnMargin: CGFloat {
        return delegate?.columnMarginInWaterflowLayout?(self) ?? 10
    }
    /// 行间距
    var rowMargin: CGFloat {
        return delegate?.rowMarginInWaterflowLayout?(self) ?? 10
    }
    /// 当前内边距
    var currentEdgeInsets: UIEdgeInsets {
        return delegate?.edgeInsetsInWaterflowLayout?(self) ?? UIEdgeInsets(top: 10,left: 10,bottom: 10,right: 10) //内边距
    }
    /// 布局属性数组
    private var attrsArray = [UICollectionViewLayoutAttributes]()
    /// 存放所有列的当前高度
    private var columnHeights = [CGFloat]()
    /// 内容的高度
    private var contentHeight:CGFloat = 0
    ///MARK: - 重写系统方法
    override func prepareLayout() {
        super.prepareLayout()
        // 清除以前计算的所有高度
        columnHeights.removeAll()
        
        for _ in 0..<columnCount {
            columnHeights.append(currentEdgeInsets.top)
        }
        // 清除之前所有的布局属性
        attrsArray.removeAll()
        // 开始创建每一个cell对应的布局属性
        let count = collectionView!.numberOfItemsInSection(0)
        for i in 0..<count {
            let indexPath = NSIndexPath(forItem: i, inSection: 0)
            // 获取indexPath位置cell对应的布局属性
            let attrs = layoutAttributesForItemAtIndexPath(indexPath)
            attrsArray.append(attrs!)
        }
    }
    ///决定cell的排布
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attrsArray
    }
    ///返回indexPath位置cell对应的布局属性
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
        let collectionViewW = collectionView?.frame.size.width
        // 设置布局属性的frame
        let w = (collectionViewW! - currentEdgeInsets.left - currentEdgeInsets.right - CGFloat(columnCount - 1) * columnMargin) / CGFloat(columnCount)
        let h = delegate?.waterflowLayout(self, itemIndex: indexPath.item, itemWidth: w)
        // 找出高度最短的那一列
        var destColumn = 0
        var minColumnHeight = columnHeights[0]
        for i in 1..<columnCount {
            // 取得第i列的高度
            let columnHeight = columnHeights[i]
            if minColumnHeight > columnHeight {
                minColumnHeight = columnHeight
                destColumn = i
            }
        }
        
        let x = currentEdgeInsets.left + CGFloat(destColumn) * (w + columnMargin)
        var y = minColumnHeight
        if y != currentEdgeInsets.top {
            y += rowMargin
        }
        attrs.frame = CGRectMake(x, y, w, h!)
        
        // 更新最短那列的高度
        columnHeights[destColumn] = CGRectGetMaxY(attrs.frame)
        
        // 记录内容的高度
        let columnHeight = columnHeights[destColumn]
        if contentHeight < columnHeight {
            contentHeight = columnHeight
        }
        return attrs
    }
    
    ///collectionView的ContentSize
    override func collectionViewContentSize() -> CGSize {
        return CGSizeMake(0, contentHeight + currentEdgeInsets.bottom)
    }
}

OC版

在.h文件中

//  FZXWaterfallsLayout.h
//
//  功用: 瀑布流的布局

#import <UIKit/UIKit.h>

@class FZXWaterfallsLayout;

// 2.构造方法,使得外界通过重写代理方面,控制细节
@protocol FZXWaterfallsLayoutDelegate <NSObject>

@required
/**
 *  返回每个item的宽高 必须实现
 */
- (CGFloat)waterflowLayout:(FZXWaterfallsLayout *)waterflowLayout heightForItemAtIndex:(NSUInteger)index itemWidth:(CGFloat)itemWidth;

//可选实现
@optional
/**
 *  返回列数
 */
- (CGFloat)columnCountInWaterflowLayout:(FZXWaterfallsLayout *)waterflowLayout;
/**
 *  返回列间距
 */
- (CGFloat)columnMarginInWaterflowLayout:(FZXWaterfallsLayout *)waterflowLayout;
/**
 *  返回行间距
 */
- (CGFloat)rowMarginInWaterflowLayout:(FZXWaterfallsLayout *)waterflowLayout;
/**
 *  返回边缘间距 上 左 下 右
 */
- (UIEdgeInsets)edgeInsetsInWaterflowLayout:(FZXWaterfallsLayout *)waterfallLayout;

@end

@interface FZXWaterfallsLayout : UICollectionViewLayout

/**
 *  设置代理,使得外界使用代理方面控制内部显示细节
 */
@property (nonatomic, weak) id<FZXWaterfallsLayoutDelegate> delegate;

@end

在.m文件中

//  FZXWaterfallsLayout.m
//
//  Created by FZX on 16/5/20.
//
//  功用: 瀑布流的布局

#import "FZXWaterfallsLayout.h"

/** 默认的列数 */
static const NSInteger FZXDefaultColumnCount = 2;
/** 每一列之间的间距 */
static const CGFloat FZXDefaultColumnMargin = 10;
/** 每一行之间的间距 */
static const CGFloat FZXDefaultRowMargin = 10;
/** 边缘间距 */
static const UIEdgeInsets FZXDefaultEdgeInsets = {10, 10, 10, 10};

@interface FZXWaterfallsLayout ()
/** 存放所有cell的布局属性 */
@property (nonatomic, strong) NSMutableArray *attrsArray;
/** 存放所有列的当前高度 */
@property (nonatomic, strong) NSMutableArray *columnHeights;
/** 内容的高度 */
@property (nonatomic, assign) CGFloat contentHeight;

// 提供和重写get方法,实现对代理方法的实时监控
- (CGFloat)rowMargin;
- (CGFloat)columnMargin;
- (NSInteger)columnCount;
- (UIEdgeInsets)edgeInsets;

@end

@implementation FZXWaterfallsLayout

#pragma mark - 初始化
- (void)prepareLayout
{
    [super prepareLayout];
    
    self.contentHeight = 0;
    
    // 清除以前计算的所有高度
    [self.columnHeights removeAllObjects];
    
    for (NSInteger i = 0; i < self.columnCount; i++)
    {
        [self.columnHeights addObject:@(self.edgeInsets.top)];
    }
    
    
    // 清除之前所有的布局属性
    [self.attrsArray removeAllObjects];
    // 开始创建每一个cell对应的布局属性
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (NSInteger i = 0; i < count; i++)
    {
        // 创建位置
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        // 获取indexPath位置cell对应的布局属性
        UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
        [self.attrsArray addObject:attrs];
    }
}

#pragma mark - 决定cell的排布
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArray;
}

#pragma mark - 返回indexPath位置cell对应的布局属性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建布局属性
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    // collectionView的宽度
    CGFloat collectionViewW = self.collectionView.frame.size.width;
    
    // 设置布局属性的frame
    CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
    CGFloat h = [self.delegate waterflowLayout:self heightForItemAtIndex:indexPath.item itemWidth:w];
    
    // 找出高度最短的那一列
    NSInteger destColumn = 0;
    CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
    for (NSInteger i = 1; i < self.columnCount; i++) {
        // 取得第i列的高度
        CGFloat columnHeight = [self.columnHeights[i] doubleValue];
        
        if (minColumnHeight > columnHeight) {
            minColumnHeight = columnHeight;
            destColumn = i;
        }
    }
    
    CGFloat x = self.edgeInsets.left + destColumn * (w + self.columnMargin);
    CGFloat y = minColumnHeight;
    if (y != self.edgeInsets.top) {
        y += self.rowMargin;
    }
    attrs.frame = CGRectMake(x, y, w, h);
    
    // 更新最短那列的高度
    self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
    
    // 记录内容的高度
    CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];
    if (self.contentHeight < columnHeight) {
        self.contentHeight = columnHeight;
    }
    return attrs;
}

#pragma mark - collectionView的ContentSize
- (CGSize)collectionViewContentSize
{
    return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}

#pragma mark - 设置默认的一些属性
//列数
-(NSInteger)columnCount
{
    if ([self.delegate respondsToSelector:@selector(columnCountInWaterflowLayout:)])
    {
        return [self.delegate columnCountInWaterflowLayout:self];
    }
    else
    {
        return FZXDefaultColumnCount;
    }
}
//每一列的间距
-(CGFloat)columnMargin
{
    if ([self.delegate respondsToSelector:@selector(columnMarginInWaterflowLayout:)])
    {
        return [self.delegate columnMarginInWaterflowLayout:self];
    }
    else
    {
        return FZXDefaultColumnMargin;
    }
}
//行间距
-(CGFloat)rowMargin
{
    if ([self.delegate respondsToSelector:@selector(rowMarginInWaterflowLayout:)])
    {
        return [self.delegate rowMarginInWaterflowLayout:self];
    }
    else
    {
        return FZXDefaultRowMargin;
    }
}
//边缘
-(UIEdgeInsets)edgeInsets
{
    if ([self.delegate respondsToSelector:@selector(edgeInsetsInWaterflowLayout:)])
    {
        return [self.delegate edgeInsetsInWaterflowLayout:self];
    }
    else
    {
        return FZXDefaultEdgeInsets;
    }
}

#pragma mark - 懒加载
-(NSMutableArray *)attrsArray
{
    if (!_attrsArray) {
        _attrsArray = [NSMutableArray array];
    }
    return _attrsArray;
}

-(NSMutableArray *)columnHeights
{
    if (!_columnHeights) {
        _columnHeights = [NSMutableArray array];
    }
    return _columnHeights;
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容