以前在网上看到过(自己也实现过)使用oc
写的基于scrollView实现的瀑布流,现在自己的项目都由swift编写了,所以趁有时间,把以前的oc
项目转一下swift
好了。
1.新建一个 waterflow 继承至 UIScrollView,创建一个 WaterflowViewCell继承至 UIView
WaterflowViewCell中,需创建 identifier 属性,用于 cell 复用
class WaterflowViewCell: UIView {
var identifier: String?
}
在waterflow中...
a).声明一个可变数组cellFrames
,用于存放所有 cell 的 frame;
b).声明一个可变字典displayingCells
,用于存放正在显示的 cell, 字典的 key 为 index,value 为 cell 对象;
c).声明一个可变的集合reusableCells
,用于存放所有离开屏幕的 cell。
因为不需要公开,所有设置为私有
fileprivate lazy var cellFrames = NSMutableArray()
fileprivate lazy var displayingCells = NSMutableDictionary()
fileprivate lazy var reusableCells = NSMutableSet()
2.创建一个遮罩层,用于当用户点击cell 之后,展示点击效果
fileprivate lazy var matteView: UIView = {
var view = UIView()
view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.1)
return view
}()
3.可以考虑模仿 tableView,设置相应的数据源方法和代理方法
数据源方法和代理方法此处分别设置了三个,考虑的项目的实际情况,暂时设置这几个,或者考虑利用 collection 来实现瀑布流,下篇博客或者考虑利用 collection 来实现瀑布流,所有的动作可以使用原生提供的 API 进行开发。
a).这里的WaterflowMarginType
是相应的间隔类型的枚举类,为什么会使用@objc
?因为此枚举在代理方法中被用作参数传递了,所有需要在枚举开头加上@objc
,至于数据源方法和代理方法也加上了@objc
,目的在于设置协议的 optional 属性。
b).数据源中的第三个方法numberOfColumnsInWaterflow
要求返回所要展示的 cell 的列数,当然不实现此方法的话默认会展示三列。
c).代理方法中:
heightAtIndex
方法要求返回 cell 的高度,默认是44;
didSelectAtIndex
方法是 cell 的点击回调;
marginForType
方法返回 cell 间间隙的宽度,默认是1。
@objc enum WaterflowMarginType: Int {
case top
case bottom
case left
case right
case column
case row
}
@objc protocol WaterflowDataSource: NSObjectProtocol {
func numberOfCellsInWaterflow(waterflow: WaterflowView) -> Int
func waterflow(waterflow: WaterflowView, cellAtIndex index: Int) -> WaterflowViewCell
@objc optional func numberOfColumnsInWaterflow(waterflow: WaterflowView) -> Int
}
@objc protocol WaterflowDelegate: NSObjectProtocol {
@objc optional func waterflow(waterflow: WaterflowView, heightAtIndex index: Int) -> CGFloat
@objc optional func waterflow(waterflow: WaterflowView, didSelectAtIndex index: Int)
@objc optional func waterflow(waterflow: WaterflowView, marginForType type: WaterflowMarginType) -> CGFloat
}
4.以下是waterflowView 类的代码部分,为了代码的可读性和整洁性,其余 public 方法和 private 方法将在 waterflowView 的类扩展中实现
对于 cell 的穿件以及属性的计算将会在`willMoveToSuperview`和`layoutSubviews`中完成,`willMoveToSuperview`什么时候会被触发?
在此拓展知识,用于笔记查阅也用于提醒铭记
-(id)initWithFrame:(CGRect)frame - UIView的指定初始化方法; 总是发送给UIView去初始化, 除非是从一个nib文件中加载的;
-(id)initWithCoder:(NSCoder *)coder - 从nib文件中加载的时候发送此消息给UIView;
-(void)awakeFromNib - 在所有的nib中的对象初始化和连接后将发送此消息; 只适用于从nib加载对象; 如要重写,其中还必须调用父类的awakeFromNib;
-(void)willMoveToSuperview:(UIView *)newSuperview - 在一个子视图将要被添加到另一个视图的时候发送此消息;
-(void)willMoveToWindow:(UIWindow *)newWindow - 在一个视图(或者它的超视图)将要被添加到window的时候发送;
-(void)didMoveToSuperview - 把一个视图插入到视图层级之后发送此消息;
-(void)didMoveToWindow - 当视图获得它的window属性集的时候发送此消息.
class WaterflowView: UIScrollView {
// delegate
var dataSource: WaterflowDataSource?
var wfDelegate: WaterflowDelegate?
fileprivate lazy var cellFrames = NSMutableArray()
fileprivate lazy var displayingCells = NSMutableDictionary()
fileprivate lazy var reusableCells = NSMutableSet()
// 默认值
fileprivate let WaterflowDefaultCellH: CGFloat = 44
fileprivate let WaterflowDefaultMargin: CGFloat = 1
fileprivate let WaterflowDefaultNumberOfColumns: Int = 3
// 遮罩层
fileprivate lazy var matteView: UIView = {
var view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.1)
return view
}()
// 用于记录 cell
fileprivate var cTupe: (NSNumber?, WaterflowViewCell?)
override func willMove(toSuperview newSuperview: UIView?) {
reloadData()
}
}
5. waterflowView 中 private 方法的类扩展
a).isInScreen
方法用于判断 cell 的 frame 是否在屏幕中,这里只考虑纵向。
b).marginForType
获取 cell 间的间隙大小,<font color=purple>这里为什么不用respondsToSelector判断代理方法是否被实现?主要原因在于wfDelegate
或者是waterflow
后面的?
,如果wfDelegate
没有被代理或者waterflow
没有被实现,则会调用??
后面的WaterflowDefaultMargin
,有点类似于三目运算有没有?其他代理方法也是这个原理。</font>
// MARK: - private
extension WaterflowView {
fileprivate func isInScreen(frame: CGRect) -> Bool {
return (frame.maxY > contentOffset.y) &&
(frame.maxY < contentOffset.y + bounds.height)
}
fileprivate func marginForType(type: WaterflowMarginType) -> CGFloat {
return wfDelegate?.waterflow?(waterflow: self, marginForType: type) ?? WaterflowDefaultMargin
}
fileprivate func numberOfColumns() -> Int {
return dataSource?.numberOfColumnsInWaterflow?(waterflow: self) ?? WaterflowDefaultNumberOfColumns
}
fileprivate func heightAtIndex(index: Int) -> CGFloat {
return wfDelegate?.waterflow?(waterflow: self, heightAtIndex: index) ?? WaterflowDefaultCellH
}
}
6. waterflowView 中 public 方法的类扩展
cellWidth
方法可以获取到 cell 的宽度
func cellWidth() -> CGFloat {
let columns = numberOfColumns()
let leftM = marginForType(type: .left)
let rightM = marginForType(type: .right)
let columnM = marginForType(type: .column)
return (bounds.width - leftM - rightM - (CGFloat(columns) - 1) * columnM) / CGFloat(columns)
}
reloadData
方法代码比较长,具体看注释就可以了
func reloadData() {
/*!
displayingCells为当前屏幕显示的 cell,是一个字典,
因此通过 allValues 可获取到字典中所有的 cell 对象,
forEach方法属于 for 循环的特殊用法(在forEach闭包中,
$0表示 字典中的 value,当然也可用闭包通用形式中 {value in method} 来编写),
这里需要移除所有的 cell。
*/
displayingCells.allValues.forEach {
($0 as AnyObject).removeFromSuperview()
}
// 清空数组、字典、集合
displayingCells.removeAllObjects()
cellFrames.removeAllObjects()
reusableCells.removeAllObjects()
// 获取 cell 的总数
let cells = dataSource?.numberOfCellsInWaterflow(waterflow: self)
// waterflow 的列数
let columns = numberOfColumns()
// cell 间的间隙
let topM = marginForType(type: .top)
let bottomM = marginForType(type: .bottom)
let leftM = marginForType(type: .left)
let columnM = marginForType(type: .column)
let rowM = marginForType(type: .row)
let cellW = cellWidth()
// 创建一个空的数组,大小为columns
var maxYOfColumns: Array<CGFloat> = Array(repeating: 0.0, count: columns)
// 循环初始化所有列的最大 y 值,瀑布流中每一行的 cell 所在位置是上一行中 y 值最小的 cell
for i in 0..<columns {
maxYOfColumns[i] = 0.0
}
// cells == nil return
guard let _cells = cells else {
return
}
for i in 0..<_cells {
// 找出 y 值最小的 cell
var cellColumn = 0
var maxYOfCellColumn = maxYOfColumns[cellColumn]
for j in 1..<columns {
if maxYOfColumns[j] < maxYOfCellColumn {
cellColumn = j
maxYOfCellColumn = maxYOfColumns[j]
}
}
let cellH = heightAtIndex(index: i)
let cellX: CGFloat = leftM + CGFloat(cellColumn) * (cellW + columnM)
var cellY: CGFloat = 0.0
if maxYOfCellColumn == 0.0 {
cellY = topM
} else {
cellY = maxYOfCellColumn + rowM
}
// 把 cell 的 frame 添加到 cellFrame 数组中,并记录当前列的最大 y 值
let cellFrame = CGRect(x: cellX, y: cellY, width: cellW, height: cellH)
cellFrames.add(NSValue(cgRect: cellFrame))
maxYOfColumns[cellColumn] = cellFrame.maxY
}
var contentH = maxYOfColumns[0]
for j in 0..<columns {
if maxYOfColumns[j] > contentH {
contentH = maxYOfColumns[j]
}
}
contentH += bottomM
// 设置 scrollView 的 contentSize
contentSize = CGSize(width: 0, height: contentH)
}
layoutSubviews
每次滚动屏幕时都会触发
override func layoutSubviews() {
super.layoutSubviews()
// 索要对应位置的 cell
let cells = cellFrames.count
for i in 0..<cells {
// 取出 i index 中的 frame
let cellFrame = (cellFrames[i] as AnyObject).cgRectValue
// 优先从字典中取出 cell
var cell: WaterflowViewCell? = displayingCells[i] as? WaterflowViewCell
// 判断对应的 frame 在不在屏幕上
if isInScreen(frame: cellFrame!) {
// 如果 frame 在屏幕上,但是 cell 并没有被创建,
// 则创建 cell,并且存放进 displayingCells字典中
guard cell != nil else {
cell = dataSource?.waterflow(waterflow: self, cellAtIndex: i)
cell!.frame = cellFrame!
addSubview(cell!)
displayingCells[i] = cell
continue
}
continue
} else {
// 如果不在,则把 cell 从当前屏幕中移除,并添加到缓存中
guard let cell = cell else {
continue
}
cell.removeFromSuperview()
displayingCells.removeObject(forKey: i)
reusableCells.add(cell)
}
}
}
dequeueReusableCellWithIdentifier
cell 重用,更加 cell 的 id 查找缓存中是否有已创建的 cell,如果有则获取这个 cell 返回并从缓存中移除。
func dequeueReusableCellWithIdentifier(identifier: String) -> AnyObject? {
var reusableCell: WaterflowViewCell?
for cell in reusableCells {
let cell = cell as! WaterflowViewCell
if cell.identifier == identifier {
reusableCell = cell
break
}
}
if reusableCell != nil {
reusableCells.remove(reusableCell!)
}
return reusableCell
}
7. waterflowView 中 事件 方法的类扩展
通过touch
方法实现事件的点击,在开始点击和结束点击时,分别添加遮罩和移除遮罩,当用户手指移动时,判断当前手指是否还在对应的 cell 中,如果不在则移除遮罩
但是这里实现还是有一点点小问题,但倒不影响使用,如果有好的思路到时再补充好了...
// MARK: - action
extension WaterflowView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard wfDelegate != nil else {
return
}
let cellTupe = getCurrentTouchView(touches: touches)
let cell = cellTupe.1
guard let _cell = cell else {
return
}
cTupe = cellTupe
// 添加遮罩
matteView.frame = _cell.bounds
_cell.addSubview(matteView)
_cell.bringSubview(toFront: matteView)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let wfDelegate = wfDelegate else {
return
}
let cellTupe = getCurrentTouchView(touches: touches)
let selectIdx = cellTupe.0
if selectIdx == cTupe.0 {
let cell = cellTupe.1
// 移除遮罩
let matteV = cell?.subviews.last
matteV?.removeFromSuperview()
if (selectIdx != nil) {
wfDelegate.waterflow?(waterflow: self, didSelectAtIndex: selectIdx!.intValue)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let cellTupe = getCurrentTouchView(touches: touches)
// 如果不在点击层 移除遮罩
if cTupe.0 != cellTupe.0 {
let matteV = cTupe.1!.subviews.last
matteV?.removeFromSuperview()
} else {
// 如果在点击层且没有遮罩,添加遮罩
if cellTupe.1!.subviews.last != matteView {
matteView.frame = cellTupe.1!.bounds
cellTupe.1!.addSubview(matteView)
cellTupe.1!.bringSubview(toFront: matteView)
}
}
}
private func getCurrentTouchView(touches: Set<UITouch>) -> (NSNumber?, WaterflowViewCell?) {
let touch: UITouch = (touches as NSSet).anyObject() as! UITouch
let point = touch.location(in: self)
var selectIdx: NSNumber?
var selectCell: WaterflowViewCell?
// 获取点击层对应的 cell
for (key, value) in displayingCells {
let cell = value as! WaterflowViewCell
if cell.frame.contains(point) {
selectIdx = (key as! NSNumber)
selectCell = cell
break
}
}
return (selectIdx, selectCell)
}
}
文章如若有错误或者误导的地方,还请原谅,如果方便,欢迎留言!