利用scrollView实现瀑布流 Swift3.0

利用scrollView实现瀑布流 Swift3.0_第1张图片

Origin blog

以前在网上看到过(自己也实现过)使用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 间的间隙大小,这里为什么不用respondsToSelector判断代理方法是否被实现?主要原因在于wfDelegate或者是waterflow后面的?,如果wfDelegate没有被代理或者waterflow没有被实现,则会调用??后面的WaterflowDefaultMargin,有点类似于三目运算有没有?其他代理方法也是这个原理。

// 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 = Array(repeating: 0.0, count: columns)
   // 循环初始化所有列的最大 y 值,瀑布流中每一行的 cell 所在位置是上一行中 y 值最小的 cell
   for i in 0.. 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..

dequeueReusableCellWithIdentifiercell 重用,更加 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, 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, 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, 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) -> (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)
    }
    
}

文章如若有错误或者误导的地方,还请原谅,如果方便,欢迎留言!

Code demo · github

你可能感兴趣的:(利用scrollView实现瀑布流 Swift3.0)