美文网首页UIKitiOSiOS技术
手把手教你使用Layout写瀑布流

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

作者: mkb2 | 来源:发表于2016-09-16 16:18 被阅读3012次

    思路:
    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)
        }
        
    }
    
    
    最后变成了这样,实现了高度的自定义话,其实和属性差不多

    代码地址

    相关文章

      网友评论

      本文标题:手把手教你使用Layout写瀑布流

      本文链接:https://www.haomeiwen.com/subject/naqiettx.html