手把手教你使用Layout写瀑布流

思路:
0.明确自定义布局的核心方法:layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes],他是用来显示cell的布局的,所有的cell,但是那,这个方法可能多次调用,所以,创建的时候要在prepare方法中写,但是,返回attribute有专门的方法,计算设置attire的各种属性--方法是layoutAttributesForItemAtIndexPath,我们需要啥属性,滴啊用他,然后在prepare获取每一个属性就好

1.继承自UICollectionViewLayout创建一个新的布局对象WFWaterFlowLayout
2.写出数据源方法,给定colletionView这个布局
3.重写WFWaterFlowLayout中的四个方法,显示出基本的样式
4.重构WFWaterFlowLayout方法,让其性能更高
5.计算cell的尺寸,核心计算
6.显示数据
7.对项目的接口在做处理,优化项目

具体实现步骤


1.继承自UICollectionViewLayout创建一个新的布局对象WFWaterFlowLayout
import UIKit

class WFWaterFlowLayout: UICollectionViewLayout {

}

2.写出数据源方法,给定colletionView这个布局
在storyBoard上设置colletionView和layout
//MARK : - 数据源方法
extension WFViewController:UICollectionViewDataSource{
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 50
    }
    
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(SFImageCellIdent, forIndexPath: indexPath)
        return cell
    }
}
3.重写WFWaterFlowLayout中的四个方法,显示出基本的样式
import UIKit

class WFWaterFlowLayout: UICollectionViewLayout {

    
    /**
     *  1.初始化调用的方法
     */
    override func prepareLayout() {
        super.prepareLayout()
    }
    
    /**
     *  2.决定cell展示布局的数组
     */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return nil
    }
    
    /**
     *  3.如果你是继承自“UICollectionViewLayout”的话,那么最好实现方法,否则可能出错
         该方法的作用是返回当前indexPath位置的布局属性
     */
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        return nil
    }
    
    /**
     *  4.当我们继承自"UICollectionViewLayout",那么他是不会滑动的,所以我们要给他设置一个contenSize来确定滑动的范围
     */
    override func collectionViewContentSize() -> CGSize {
        return CGSizeMake(0, 100)
    }
    
}
4.重构WFWaterFlowLayout方法,让其性能更高
  //MARK: - 创建一个数组,用来盛放属性对象
    private lazy var attributes = [UICollectionViewLayoutAttributes]()
    
    /**
     *  1.初始化调用的方法
     */
    override func prepareLayout() {
        super.prepareLayout()
        //每一次调用reload方法,如果数组不删除,那么会越来越多数据,所以我们要去清空
        attributes.removeAll()
        
        //2.1 创建含有属性的数组
        //流水布局一般是有1组,我们直接获取个数就好
        let count = collectionView?.numberOfItemsInSection(0)
        
        for index in 0 ..< count!
        {
            //2.2 创建位置
            let indexPath = NSIndexPath.init(forItem: index, inSection: 0)
            //2.3 创建布局属性
            let  attri = UICollectionViewLayoutAttributes(forCellWithIndexPath:indexPath)
            //2.4 设置属性,给frame一个随机数
            let aX = CGFloat(arc4random_uniform(300))
            let aY = CGFloat(arc4random_uniform(300))
            let aW = CGFloat(arc4random_uniform(300))
            let aH = CGFloat(arc4random_uniform(300))
            attri.frame = CGRectMake( aX, aY, aW, aH)
            attributes.append(attri)
        }
    }
    
    /**
     *  2.决定cell展示布局的数组
     */
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attributes
    }
    
    /**
     *  3.如果你是继承自“UICollectionViewLayout”的话,那么最好实现方法,否则可能出错
         该方法的作用是返回当前indexPath位置的布局属性
     */
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        return attributes[indexPath.row]
    }
    
    /**
     *  4.当我们继承自"UICollectionViewLayout",那么他是不会滑动的,所以我们要给他设置一个contenSize来确定滑动的范围
     */
    override func collectionViewContentSize() -> CGSize {
        return CGSizeMake(10, 100)
    }

刚才搞错了一个方法let indexPath = NSIndexPath.init(forItem: index, inSection: 0),写错成了let indexPath = NSIndexPath(index:index)一直报错

2016-09-16 14:33:08.890 WaterFlow[2721:225067] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x7febfa61c680> {length = 1, path = 0}'

一定要注意哈

现在的样子

注意:今天没有重写shouldInvalidateLayoutForBoundsChange这个方法,是因为,我们继承的是collectionViewLayout,默认是真,之前调用,是因为继承的是UICollectionViewFlowLayout,设置的是假

5.计算cell的尺寸,计算每一列的高度

步骤
1.获取collectionView的内边距,item之间的间距等
2.计算cell的宽度,随机给他一个高度
3.通过一个数组,保存所有列的高度,用于比较最小的y值和更新 contentSize

定义几个常量

let WFVerticalMargin:CGFloat = 10
let WFHorMargin:CGFloat = 10
let WFEdgeInsets:UIEdgeInsets = UIEdgeInsetsMake(10, 10, 10, 10)

//oc中这写
/** 边缘间距 */
static const UIEdgeInsets WFDefaultEdgeInsets = {10, 10, 10, 10};
// 每一次更新,我们都要记得删除过去的缓存,重新计算
    override func prepareLayout() {
        super.prepareLayout()
        
        //流水布局一般是有1组,我们直接获取个数就好
        let count = collectionView?.numberOfItemsInSection(0)
        
        //每一次调用reload方法,如果数组不删除,那么会越来越多数据,所以我们要去清空
        attributes.removeAll()
        
         /// 1.1 每一次更新,都要先去出缓存的列的高度
        colunmsHeightArr .removeAllObjects()
         /// 1.2 清除之后,还要给他们一个默认的高度
        for _  in 0 ..< count!
        {
               colunmsHeightArr.addObject(WFEdgeInsets.top)
        }
}
 /**
     *  3.如果你是继承自“UICollectionViewLayout”的话,那么最好实现方法,否则可能出错
         该方法的作用是返回当前indexPath位置的布局属性
     */
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let  attri = UICollectionViewLayoutAttributes(forCellWithIndexPath:indexPath)
        
        //1.计算frame
        
        //2.4 设置属性,给frame一个随机数
                /// 2.4.1设置x,y值的根据就是讲cell放置到最小的那一列中
        
         /// 保存最短列的列号 
        var colunmIndex = 0 //默认0
        var colunmMinHeight = colunmsHeightArr[colunmIndex] as! CGFloat//默认最短的列高度是第一列
        for col in 1..<WFDefaultColunmsNum {
            let currentColHeight = (colunmsHeightArr[col] as! CGFloat)
            if colunmMinHeight > currentColHeight
            {
                colunmMinHeight = currentColHeight
                colunmIndex = col
            }
        }
        
        
        //几个间距的和
        let totalMagin = CGFloat(WFDefaultColunmsNum - 1)*WFHorMargin
        let aW = (WFScreenWidth - WFEdgeInsets.left - WFEdgeInsets.right - totalMagin)/CGFloat(WFDefaultColunmsNum)
        let aH = CGFloat(arc4random_uniform(60)) + 30
        let aX = WFEdgeInsets.left + CGFloat(colunmIndex) * (WFHorMargin + aW)
        
        var aY = colunmMinHeight + WFVerticalMargin
        if aY != WFEdgeInsets.top {
            aY = aY + WFVerticalMargin
        }
        attri.frame = CGRectMake( aX, aY, aW, aH)
        
        //更新保存高度的数组
        colunmsHeightArr.replaceObjectAtIndex(colunmIndex, withObject: CGRectGetMaxY(attri.frame))
        
        return attri
    }

    /**
     *  4.当我们继承自"UICollectionViewLayout",那么他是不会滑动的,所以我们要给他设置一个contenSize来确定滑动的范围
     */
    override func collectionViewContentSize() -> CGSize {
        var maxY = colunmsHeightArr[0] as! CGFloat
        for col in 1..<colunmsHeightArr.count {
            let currentColHeight = (colunmsHeightArr[col] as! CGFloat)
            if maxY < currentColHeight
            {
                maxY = currentColHeight
            }
        }
        
        return CGSizeMake(WFScreenWidth, maxY + WFEdgeInsets.bottom)
    }
}
最后的效果
6.设置数据

使用pod,设置框架

platform:ios,'8.0'
use_frameworks!
pod 'MJRefresh'
pod 'SDWebImage'
pod 'MJExtension'

1.生成一个cell- SFImageCell
2.通过plist文件来加载一个数组的模型 shops = WFShopModel.mj_objectArrayWithFilename("1.plist")
3.设置数据
4.设置上啦刷新,下啦加载
5.根据图片的宽度,设置等比例高度

设置下啦刷新,上啦加载,注意使用的对象,和延迟两秒的GCD用法

    private func setupRefreshView(){
      collectionView.mj_header = MJRefreshNormalHeader.init(refreshingBlock: { 

        self.shops.removeAllObjects()
        let data = WFShopModel.mj_objectArrayWithFilename("1.plist")
        self.shops.addObjectsFromArray(data as [AnyObject])
        self.collectionView.reloadData()
        self.collectionView.mj_header.endRefreshing()
      })
        collectionView.mj_footer = MJRefreshAutoNormalFooter.init(refreshingBlock: {
            //要延迟几秒,才会有小菊花
            let time: NSTimeInterval = 2.0
            let delay = dispatch_time(DISPATCH_TIME_NOW,
                Int64(time * Double(NSEC_PER_SEC)))
            dispatch_after(delay, dispatch_get_main_queue()) {
                let data = WFShopModel.mj_objectArrayWithFilename("1.plist")
                self.shops .addObjectsFromArray(data as [AnyObject])
                self.collectionView.reloadData();
                self.collectionView.mj_footer.endRefreshing()
            }
        });
        collectionView.mj_header.beginRefreshing()
        self.collectionView.mj_footer.hidden = false
    }
加载之后,合并数据的时候还是有问题,是因为我们没有根据图片比例设置宽度

现在去根据图片的比例设置cell 的高度
过去的高度是 let aH = CGFloat(arc4random_uniform(60)) + 30,所以是不对的

在layout勒种天机一个属性

//计算cell高度
        let shop = shops?[indexPath.row] as? WFShopModel
        var iHeight:CGFloat =  0
        if shop != nil {
             iHeight =  aW * (shop?.h)!/(shop?.w)!
        }
        let aH = iHeight

在加载数据的时候,我们都要更新一下shops数组

//layout 是我从storyBoard上拉线过来的,属于colletionView
        self.layout.shops = self.shops
这就基本写好了

但是,现在的只是能够显示WFShopModel,在项目中,我们称之为,模块,并不能当做开源库使用,因为他的功能太单一。
思考?为毛线UITableView功能那么强大,什么格式都能显示,他们如何做的这么强大?因为有代理和数据源,现在我们看看如何通过代理,给瀑布流拓展成能让所有人使用的开源库


本身可以将所有的方法全部归类到代理中,但是还是决定使用一个数据源方法,更加直观。


先写出来数据源和代理方法,水平有限,google了一些option和必须实现的方法,但是感觉麻烦,就不写了,其实tableView就有必须实现,和可实现的方法,你们自己找吧~

protocol WFWaterFlowLayoutDataSource:NSObjectProtocol{
    
    /**
     :param: waterFlowLayout self
     :param: width           提供给外边,cell的宽度
     :returns:返回来cell 的高度
     */
    func waterFlowLayout(waterFlowLayout: WFWaterFlowLayout, itemWidth width: CGFloat,indexPath:NSIndexPath) -> CGFloat?
    /**
     :param: waterFlowLayout self
     
     :returns: 一共几列
     */
    func columnOfWaterFlowLayout(waterFlowLayout: WFWaterFlowLayout) -> NSInteger?
}

protocol WFWaterFlowLayoutDelegate:NSObjectProtocol {
    /**
     通过代理返回过来colletionView的内边距
     :param: waterFlowLayout self
     */
     func marginOfSectionInsert(waterFlowLayout: WFWaterFlowLayout) -> UIEdgeInsets?
    /**
     :param: waterFlowLayout self
     
     :returns: 返回item之间竖直间距
     */
    func itemVerticalMargin(waterFlowLayout: WFWaterFlowLayout) -> CGFloat?
    /**
     :param: waterFlowLayout self
     :returns:  返回item之间水平的间距
     */
    func itemHorMargin(waterFlowLayout: WFWaterFlowLayout) -> CGFloat?
    
}

定义一个代理变量和数据源变量,以及快速获取变量的值的函数

    weak var dataSource:WFWaterFlowLayoutDataSource?
    weak var delegate:WFWaterFlowLayoutDelegate?
    
    //MARK - get 方法,获取具体的数据
    private func verticalMarign() -> CGFloat
    {
        if ((delegate?.itemVerticalMargin(self)) != nil)
        {
            return (delegate?.itemVerticalMargin(self))!
        } else{
            return WFVerticalMargin
        }
    }
    
    
    private func horMargin() -> CGFloat
    {
        if ((delegate?.itemHorMargin(self)) != nil) {
            return (delegate?.itemHorMargin(self))!
        }else{
            return WFHorMargin
        }
    }
    
    private func sectionInset() -> UIEdgeInsets{
        if ((delegate?.marginOfSectionInsert(self)) != nil) {
           return (delegate?.marginOfSectionInsert(self))!
        }else{
            return WFEdgeInsets
        }
    }
    
    private func numberOfSection() -> NSInteger{
        if ((dataSource?.columnOfWaterFlowLayout(self)) != nil) {
            return (dataSource?.columnOfWaterFlowLayout(self))!
        }else{
            return WFDefaultColunmsNum
        }
    }

然后将那些东西全部替换,实现代理方法和数据源方法

extension WFViewController:WFWaterFlowLayoutDataSource,WFWaterFlowLayoutDelegate{
    func waterFlowLayout(waterFlowLayout: WFWaterFlowLayout,
                                          itemWidth width: CGFloat,
                                                   indexPath: NSIndexPath) -> CGFloat? {
        let shop = shops[indexPath.row] as! WFShopModel
        return width / (shop.w/shop.h)
    }
    
    func itemHorMargin(waterFlowLayout: WFWaterFlowLayout) -> CGFloat? {
        return 20
    }
    
    func itemVerticalMargin(waterFlowLayout: WFWaterFlowLayout) -> CGFloat? {
        return 30
    }
    
    func columnOfWaterFlowLayout(waterFlowLayout: WFWaterFlowLayout) -> NSInteger? {
        return 3
    }
    
    func marginOfSectionInsert(waterFlowLayout: WFWaterFlowLayout) -> UIEdgeInsets? {
        return UIEdgeInsetsMake(12, 34, 10, 20)
    }
    
}

最后变成了这样,实现了高度的自定义话,其实和属性差不多

代码地址

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

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,705评论 1 92
  • 翻译自“Collection View Programming Guide for iOS” 0 关于iOS集合视...
    lakerszhy阅读 3,782评论 1 22
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,085评论 25 707
  • 一、简介 学习和实践Hive中,使用的都是CLI或者hive –e的方式,该方式仅允许使用HiveQL执行查询、更...
    献给记性不好的自己阅读 7,383评论 0 2
  • 简介 HashMap是一个比较常用的键值对集合,在开发中常用于映射关系。以下分析是基于Android中API25下...
    范锦浩阅读 224评论 0 0