iOS开发UI篇-几句代码实现瀑布流

瀑布流.gif

在开发中,瀑布流用的挺频繁的,尤其是在做一些电商应用的时候,由于错落有致的外观能防止用户在浏览商品时所产生的视觉疲劳,瀑布流就应运而生了,废话不多说,直接上Demo.

在这个Demo中,我对瀑布流布局做了个封装,实现瀑布流只需遵循下协议,实现几个代理方法即可,由于最近swift用得比较频繁,这个Demo就用swift来演示了.(需要OC版的可以私信我).

一.布局核心实现


** 要实现瀑布流相当于是自己写一个布局,因此需要继承于UICollectionViewLayout,重写里面的几个方法来确定布局:**

  • func prepareLayout()这个方法中对布局进行一些初始化的操作
// 初始化布局方法
override func prepareLayout() {
    super.prepareLayout()
    // 清空之前所有的列数的高度数据,并初始化
    columnHeightArray.removeAll()
    for _ in 0..<columnCount() {
        columnHeightArray.append(0)
    }
  
    // 清空之前所有cell的布局属性
    itemAttributeArray.removeAll()
    
    // collectionView中的cell的个数
    let count = collectionView?.numberOfItemsInSection(0) ?? 0
    
    for i in 0..<count {
        
        let indexPath = NSIndexPath(forItem: i, inSection: 0)
        
        // 根据indexPath设置对应的layoutAttributes
        let layoutAttribute = layoutAttributesForItemAtIndexPath(indexPath)!
        
        itemAttributeArray.append(layoutAttribute)
    }
    
}
  • 在这个方法中确定指定范围内(rect)的item的布局属性,由于指定范围内的item可能有多个,所以返回一个数组
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return itemAttributeArray
}
  • 确定每个item的布局属性(核心代码),在这个方法里确定每个item的frame
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
    // 创建单个cell的布局属性
    let layoutAttribute = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
    
    // 设置布局属性
    
    // collectionView的宽度
    let collectionViewW = collectionView!.frame.size.width
    // 一行中所有item的总宽度
    let itemWs = collectionViewW - collectViewEdgeInsets().left - collectViewEdgeInsets().right - CGFloat(columnCount() - 1) * columnMargin()
    
    // 找出最矮的一列
    // 假设第一列最矮
    var minHeight : CGFloat = columnHeightArray[0]
    var desRow : Int = 0
    for i in 1..<columnHeightArray.count {
        let height = columnHeightArray[i]
        
        if height < minHeight {
            minHeight = height
            desRow = i
        }
    }
    
    // 确定item的frame
    let w : CGFloat = itemWs /  CGFloat(columnCount())
    let x : CGFloat = collectViewEdgeInsets().left + (columnMargin() + w) * CGFloat(desRow)
    let y : CGFloat = columnHeightArray[desRow] + rowMargin()
    let h : CGFloat = (delegate?.waterFlowLayout(self, heightForItemAtIndex: indexPath.item, itemWidth: w))!
    layoutAttribute.frame = CGRect(x: x, y: y, width: w, height: h)
    
    // 更新列高度数据
    columnHeightArray[desRow] = CGRectGetMaxY(layoutAttribute.frame)
    
    return layoutAttribute
}
  • 确定collectionView的contentSize
override func collectionViewContentSize() -> CGSize {
    // 找出最高的一列
    // 假设第一列最高
    var maxHeight : CGFloat = columnHeightArray[0]
    var desRow : Int = 0
    for i in 1..<columnHeightArray.count {
        let height = columnHeightArray[i]
        
        if height > maxHeight {
            maxHeight = height
            desRow = i
        }
    }
    
    return CGSize(width: 0, height: columnHeightArray[desRow] + rowMargin())
}

二.布局的一些相关数据(item的大小,item间的间距等)


  • 对于和布局所需的数据(比如item的高度),需要由实际显示的图片大小来决定,在这里数据的传输有两种方式:

  • 第一种是在布局类对象里定义一个item的高度属性,在外界用到的时候给这个属性赋值,这个方式虽然方便快捷,但一方面,代码的耦合性太强了,不一定每次显示的东西都是一样的,换到别的地方可能就不能用了;另一方面,定义了这个属性相当于给外界反复修改item的高度提供了可能,而item高度一改,内部瀑布流的布局又要重新刷新,耗性能

  • 另一种方式就是通过代理来实现数据的传输,只需要将布局所需的属性,定义成代理方法,外界实现代理方法,实现数据传输,这种方法完美的消除了耦合性,而且能控制用户的反复输入,方法的调用时机也有布局对象内部决定,本案例采取的就是这个方法

  • 定义一份协议用来让外界传递瀑布流所需的相应的数据

@objc protocol ZWFWaterFlowLayoutDelegate {
    // 每个item的高度
    func waterFlowLayout(waterLayout: ZWFWaterFlowLayout, heightForItemAtIndex index: NSInteger, itemWidth : CGFloat) -> CGFloat
    // collectionView的列数
    optional func columnCountInWaterFlow(waterLayout : ZWFWaterFlowLayout) -> Int
    // item间的列间距
    optional func columnMarginInWaterFlow(waterLayout : ZWFWaterFlowLayout) -> CGFloat
    // item间的行间距
    optional func rowMarginInWaterFlow(waterLayout : ZWFWaterFlowLayout) -> CGFloat
    // collectionView的内边距
    optional func collectViewEdgeInWaterFlow(waterLayout : ZWFWaterFlowLayout) -> UIEdgeInsets
}
  • 将属性定义成方法是为了方便集中管理,以及设置默认值
// MARK:- 一些基本属性(由外界提供)
extension ZWFWaterFlowLayout {
    // MARK:- 列间距
    func columnMargin() -> CGFloat {
        // 校验有没有代理
        if delegate == nil {  // 默认列间距为10
            return 10
        }
        // 校验代理有没有实现方法
        guard let margin = delegate!.columnMarginInWaterFlow?(self) else { // 默认列间距为10
            return 10
        }
        return margin
    }
    // MARK:- 列数
    func columnCount() -> Int {
        // 校验有没有代理
        if delegate == nil { // 默认列数为3
            return 3
        }
        // 校验代理有没有实现方法
        guard let count = delegate!.columnCountInWaterFlow?(self) else { // 默认列数为3
            return 3
        }
        return count
    }
    // MARK:- 行间距
    func rowMargin() -> CGFloat {
        // 校验有没有代理
        if delegate == nil {  // 默认行间距为10
            return 10
        }
        // 校验代理有没有实现方法
        guard let margin = delegate!.rowMarginInWaterFlow?(self) else {  // 默认行间距为10
            return 10
        }
        return margin
    }
    // MARK:- 四边内间距
    func collectViewEdgeInsets() -> UIEdgeInsets {
        // 校验有没有代理
        if delegate == nil {  // 默认四周内边距为10
            return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        }
        // 校验代理有没有实现方法
        guard let edgeInsets = delegate!.collectViewEdgeInWaterFlow?(self) else {  // 默认四周内边距为10
            return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        }
        return edgeInsets
    }
}

三.布局的使用


import UIKit

// MARK:- 主函数
class ViewController: UIViewController {
    
    // MARK:- 懒加载控件
    private lazy var collectionView = UICollectionView()
    // 所有的商品数据
    private lazy var shops = [ShopItem]()
    
    // cell的标识
    let ID : String = "cell"
    
    // MARK:- 系统回调函数
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 初始化collectionView
        setupCollectionView()
       
        // 加载数据
        setupRefresh()
    }
}

// MARK:- 加载数据
extension ViewController {
    
    func setupRefresh() {
        // 设置下拉刷新
        collectionView.header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: "loadData")
        collectionView.header.beginRefreshing()
        
        // 设置上拉加载更多数据
        collectionView.footer = MJRefreshAutoNormalFooter(refreshingTarget: self, refreshingAction: "loadMoreData")
        collectionView.footer.beginRefreshing()
    }
    
    // 加载数据
    @objc private func loadData() {
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(UInt64(2.0) * NSEC_PER_SEC)), dispatch_get_main_queue()) { () -> Void in
            
            let path = NSBundle.mainBundle().pathForResource("1.plist", ofType: nil)!
            let shopArray = NSArray(contentsOfFile: path)!
            
            // 清空保存的所有数据
            self.shops.removeAll()
            
            for dict in shopArray {
                let shop = ShopItem.init(dict: dict as! [String : NSObject])
                self.shops.append(shop)
            }
            
            self.collectionView.reloadData()
            self.collectionView.header.endRefreshing()
        }
        
    }
    
    // 加载更多数据
    @objc private func loadMoreData() {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(UInt64(2.0) * NSEC_PER_SEC)), dispatch_get_main_queue()) { () -> Void in
            
            let path = NSBundle.mainBundle().pathForResource("1.plist", ofType: nil)!
            let shopArray = NSArray(contentsOfFile: path)!
            for dict in shopArray {
                let shop = ShopItem.init(dict: dict as! [String : NSObject])
                self.shops.append(shop)
            }
            
            self.collectionView.reloadData()
            self.collectionView.footer.endRefreshing()
        }
    }
    
}


// MARK:- 初始化collectionView
extension ViewController {
    
    private func setupCollectionView() {
        
        // 创建瀑布流布局
        let waterFlowLayout = ZWFWaterFlowLayout()
        waterFlowLayout.delegate = self
        
        // 创建collectionView
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: waterFlowLayout)
        
        collectionView.dataSource = self
        collectionView.backgroundColor = UIColor.whiteColor()
        
        // 注册cell
        collectionView.registerClass(ShopCell.self, forCellWithReuseIdentifier: ID)
        
        self.collectionView = collectionView
        view.addSubview(collectionView)
    }
}

// MARK:- UICollectionViewDataSource
extension ViewController : UICollectionViewDataSource {
    
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        collectionView.footer.hidden = shops.count == 0
        return shops.count
    }
    
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        // 取出cell
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(ID, forIndexPath: indexPath) as! ShopCell
        
        // 设置cell的属性
        cell.shop = shops[indexPath.item];
        
        return cell
    }
    
}

// MARK:- CollectionViewWaterLayoutDelegate
extension ViewController : ZWFWaterFlowLayoutDelegate {
    
    // 返回每个item的高度
    func waterFlowLayout(waterLayout: ZWFWaterFlowLayout, heightForItemAtIndex index: NSInteger, itemWidth : CGFloat) -> CGFloat {
        
        let shop = shops[index]
        
        return shop.h * itemWidth / shop.w
    }
    
    // 返回collectionView的列数
    func columnCountInWaterFlow(waterLayout: ZWFWaterFlowLayout) -> Int {
        return 3
    }
    
    // 返回列间距
    func columnMarginInWaterFlow(waterLayout: ZWFWaterFlowLayout) -> CGFloat {
        return 20
    }
    
    // 返回行间距
    func rowMarginInWaterFlow(waterLayout: ZWFWaterFlowLayout) -> CGFloat {
        return 20
    }
    
    // 返回collectionView的内边距
    func collectViewEdgeInWaterFlow(waterLayout: ZWFWaterFlowLayout) -> UIEdgeInsets {
        return UIEdgeInsets(top: 20, left: 10, bottom: 30, right: 20)
    }
    
}

Demo以上传github,欢迎下载,如有错漏之处,欢迎指正瀑布流源码

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

推荐阅读更多精彩内容