UIKitiOSiOS技术

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

2016-09-16  本文已影响3012人  mkb2

思路:
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)
    }
    
}

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

代码地址

上一篇 下一篇

猜你喜欢

热点阅读