![](https://img.haomeiwen.com/i2058543/e9d6de8bce2b4216.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,欢迎下载,如有错漏之处,欢迎指正瀑布流源码
网友评论