手把手教你使用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)
}
}
最后变成了这样,实现了高度的自定义话,其实和属性差不多