63 - Swift之瀑布流(UICollectionView)
一、简介
瀑布流:又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。
二、 瀑布流的实现
1、瀑布流的实现选择
瀑布流的实现可以使用 UITableView 或 UICollectionView 或 UIScrollView 等来实现瀑布流。注意: UITableView 和 UICollectionView 两个在实现瀑布流时相比 UIScrollView 实现瀑布流较为简单。同时 UICollectionView 在实现瀑布流时要比 UITableView 时 Item的样式多样和流动效果也更好。
2、 瀑布流的实现准备
根据各个实现瀑布流的控件的对比,我们选择 UICollectionView 来实现。选择 UICollectionView 就需要自定义流动布局,我们要创建一个布局类继承与 UICollectionViewFlowLayout 。同时要重写UICollectionViewFlowLayout 的下面四个方法:
-
override func prepare() ===》: 作用是重新更改Item的布局。但是在重新更改之前要首先调用 父类的该方法。
-
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? ==》: 返回每一个 Item 的 LayoutAttribute 。
-
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? ==》: 返回indexPath位置的 Item的LayoutAttributes
-
override var collectionViewContentSize: CGSize ===》 : 返回 collectionView 的滑动范围
3、 瀑布流的布局重写
1、 定义一些变量
// 创建一个代理
var delegate:WaterfallViewFlowLayoutDelegate?
// 瀑布流的列数
var WatefallColumns = 2
// 列间距
var ColumnsSpacing:CGFloat = 10.0
// 行间距
var LineSpacing:CGFloat = 10.0
// 创建存放当前加载的Cell 的布局属性
var cellLayoutAttributes : NSMutableArray?
// 创建存放Cell 所在列的高度
var cellColumnsHeights : NSMutableArray?
// cell 内容的高度
var contentHeight:CGFloat?
2、 func prepare() 方法的重写
// MARK: 重写初始化方法
override func prepare() {
// 调用父类方法
super.prepare()
// MARK: 初始化一些数据
// TODO: 内容的高度
contentHeight = 0.0
// TODO: 数组初始化
cellColumnsHeights = NSMutableArray.init(capacity: 0)
cellLayoutAttributes = NSMutableArray.init(capacity: 0)
// TODO: 我要初始化每列高度的初始值
for _ in 0 ..< WatefallColumns {
cellColumnsHeights?.add(self.sectionInset.top)
}
// TODO: 获取当前加载的Cell个数
let loadCellCount = self.collectionView?.numberOfItems(inSection: 0)
// TODO: 遍历获取的Cell得到每个Cell的LayoutAttributes,并存放到 cellLayoutAttributes 里面
for i in 0 ..< loadCellCount! {
// TODO: 获取Cell的位置
let indexPath = IndexPath.init(row: i, section: 0)
// TODO: 获取Cell的LayoutAttributes
let cellAttribute = self.layoutAttributesForItem(at: indexPath)
// TODO: 将获取的 cellAttribute 添加到 cellLayoutAttributes
cellLayoutAttributes?.add(cellAttribute!)
}
}
3、 override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? 方法的重写
// MARK: 设置Cell的位置
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// TODO: 获取Cell的宽度
let cellWidth = ((self.collectionView?.bounds.width)! - LineSpacing * CGFloat(WatefallColumns - 1)-self.sectionInset.left - self.sectionInset.right) / CGFloat(WatefallColumns)
// TODO: 获取Cell的高度
let cellHight = delegate?.waterFlowLayout(waterFlowLayout: self, indexPath: indexPath ,width: cellWidth);
// TODO: 默认cellColumnsHeights的第一个对象是高度最低的Cell
var minColumnsCellHeight = cellColumnsHeights?.firstObject as! CGFloat
// TODO: 标记第几列是Cell 最低列
var minColumnCellMark = 0
// TODO: 遍历每一列的Cell高度,获取得到最小的一个
for i in 0 ..< WatefallColumns {
let tempCellHeight = cellColumnsHeights?[i] as! CGFloat
if minColumnsCellHeight > tempCellHeight {
minColumnsCellHeight = tempCellHeight
minColumnCellMark = i
}
}
// TODO: 最低Cell的X轴的位置
let minCellHeightX = CGFloat(minColumnCellMark) * (cellWidth + ColumnsSpacing) + self.sectionInset.left
// TODO: 最低Cell的Y轴的位置
var cellHeightY = minColumnsCellHeight
if cellHeightY != self.sectionInset.top {
cellHeightY += LineSpacing
}
// TODO: 设置大小
let LayoutAttribute = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
LayoutAttribute.frame = CGRect.init(x: minCellHeightX, y: cellHeightY, width: cellWidth, height: cellHight!)
// TODO: 设置Cell 高度中,最低的Y轴位置
cellColumnsHeights?[minColumnCellMark] = LayoutAttribute.frame.maxY
// TODO: 获取Cell高度数组最小的一个
let minCellHeightY = cellColumnsHeights?[minColumnCellMark] as! CGFloat
if contentHeight! < minCellHeightY {
contentHeight = minCellHeightY
}
return LayoutAttribute
}
4、override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 的方法的重写
// MARK: 返回样式
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return (cellLayoutAttributes as! [UICollectionViewLayoutAttributes])
}
5、override var collectionViewContentSize: CGSize 的方法的重写
// MARK: 设置Cell可滑动的范围。注意:swift3.0废弃了上面这个方法,所以我们改成重写collectionViewContentSize属性
override var collectionViewContentSize: CGSize {
get {
return CGSize.init(width: (self.collectionView?.frame.width)!, height: self.maxH(cellHeight: cellColumnsHeights!))
}
set {
self.collectionViewContentSize = newValue
}
}
6、func maxH(cellHeight:NSMutableArray) -> CGFloat 函数的实现
// TODO: 获取Cell的高度
func maxH(cellHeight:NSMutableArray) -> CGFloat {
var max = cellHeight.firstObject as! CGFloat
for i in 0 ..< cellHeight.count {
if max < (cellHeight[i] as! CGFloat) {
max = cellHeight[i] as! CGFloat
}
}
return max + self.sectionInset.bottom
}
7、 WaterfallViewFlowLayoutDelegate 的代理的声明
// 创建代理
protocol WaterfallViewFlowLayoutDelegate: NSObjectProtocol {
// 获取内容的高度
func waterFlowLayout(waterFlowLayout:WaterfallViewFlowLayout,indexPath: IndexPath,width:CGFloat) -> CGFloat ;
}
三 、 瀑布流的实现
1、 plist 文件的加载获取数据
// MARK: 获取展示的数据
func getShowData() -> Void {
dataSource = NSArray.init()
let plist = Bundle.main.path(forResource: "loadData", ofType: "plist", inDirectory: nil)
dataSource = NSArray.init(contentsOfFile: plist!)
}
2、 UICollecionView 的创建 (重点)
// MARK: 创建CollectionView
func createCollectionView() -> Void {
// TODO: 设置布局对象
let flowLayout = WaterfallViewFlowLayout.init()
// TODO: 设置有多少列
flowLayout.WatefallColumns = 1
// TODO: 设置代理
flowLayout.delegate = self
// TODO: 设置Section的偏移
flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
// TODO: 设置滑动的方向
flowLayout.scrollDirection = .vertical
// TODO: 创建 CollectionView 对象
let collectionView = UICollectionView.init(frame: self.view.frame, collectionViewLayout: flowLayout)
collectionView.backgroundColor = UIColor.white
collectionView.delegate = self
collectionView.dataSource = self
// TODO:注册 Cell
collectionView .register(UICollectionViewCell.self, forCellWithReuseIdentifier: "NetWork小贱")
self.view.addSubview(collectionView)
}
3、 func waterFlowLayout(waterFlowLayout: WaterfallViewFlowLayout, indexPath: IndexPath, width: CGFloat) 函数的实现
// MARK: 代理事件
func waterFlowLayout(waterFlowLayout: WaterfallViewFlowLayout, indexPath: IndexPath, width: CGFloat) -> CGFloat {
// TODO: 获取数据对象
let dict = dataSource![indexPath.row] as! NSDictionary
// TODO: 获取图像数据,为获取高度做准备
let image = UIImage.init(named: dict["image"] as! String)
// TODO: 计算各个元素的高度(图像高 + 标题高 + 内容高)
return self.getImageHeight(image: image!, width:width) + self.getTextHeight(param: dict["title"] as! String, width: width, fontSize: 18) + self.getTextHeight(param: dict["content"] as! String, width: width, fontSize: 10)
}
4、 UICollectionViewCell 的布局创建
// TODO: UICollectionViewCell的创建
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// TODO: 获取Cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NetWork小贱", for: indexPath)
// TODO: 设置Cell 的背景色
cell.contentView.backgroundColor = UIColor.white
// TODO: 防止Cell 内的元素复用
for item in cell.contentView.subviews {
item.removeFromSuperview()
}
// TODO: 获取数据
let dict = dataSource![indexPath.row] as! NSDictionary
let image = UIImage.init(named: dict["image"] as! String)
// TODO: 创建图像对象
let imageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: cell.contentView.bounds.width, height: self.getImageHeight(image: image!, width: cell.contentView.bounds.width)))
imageView.tag = 100
imageView.image = image
cell.contentView.addSubview(imageView)
// 添加标题
let TitleLable = UILabel.init(frame: CGRect.init(x: 0, y: imageView.frame.maxY, width: cell.contentView.bounds.size.width, height: self.getTextHeight(param: dict["title"] as! String, width: cell.contentView.bounds.width, fontSize: 18)))
TitleLable.text = (dict["title"] as! String)
TitleLable.font = UIFont.systemFont(ofSize: 18)
TitleLable.numberOfLines = 0
cell.contentView.addSubview(TitleLable)
// 添加内容
let contentLable = UILabel.init(frame: CGRect.init(x: 0, y: TitleLable.frame.maxY, width: cell.contentView.bounds.width, height: self.getTextHeight(param: dict["content"] as! String, width: cell.contentView.bounds.width, fontSize: 10)))
contentLable.text = dict["content"] as? String
contentLable.font = UIFont.systemFont(ofSize: 10)
contentLable.numberOfLines = 0
cell.contentView.addSubview(contentLable)
return cell
}
5、 UICollectionViewCell 的选择处理(处理对象放大显示)
// TODO: 选择的是哪个Cell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 获取选中的Cell
let selecdCell = collectionView.cellForItem(at: indexPath)
let imageView = selecdCell?.viewWithTag(100) as! UIImageView
// 获取图像在 self.view 上的位置
let rect = selecdCell!.convert(imageView.bounds, to: view)
// 创建图像点击放大的对象
let tempView = WaterfallZoomComponentsView.init(frame: self.view.frame)
tempView.waterfallZoom(initialFrame: rect, image: imageView.image!)
self.view.addSubview(tempView)
}
6、 获取图像高度的函数和文本高度的函数
1、 图像的高度的获取
// MARK: 获取图像的高度
func getImageHeight(image:UIImage,width:CGFloat) -> CGFloat {
// 获取图像对象的宽高比
let aspectRatio = image.size.height / image.size.width
return aspectRatio * width
}
2、文本高度的获取
// MARK: 获取文本的高度
func getTextHeight(param:String,width:CGFloat,fontSize:CGFloat) -> CGFloat {
let str = param as NSString
let textSize = str.boundingRect(with: CGSize.init(width: width, height: CGFloat(MAXFLOAT)), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName:UIFont.systemFont(ofSize: fontSize),NSForegroundColorAttributeName:UIColor.black], context: nil)
return textSize.height
}
四、图像点击放大的实现
UICollectionView 中的Cell 点击放大。我们的实现是使用一个View 和 一个 UIImageView 来展示的。所以,我们要创建一个继承与UIView的类 WaterfallZoomComponentsView。
WaterfallZoomComponentsView 类的一些方法的实现
1、 override init(frame: CGRect) 方法的重写
// MARK: 从写初始化方法
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(colorLiteralRed: 0, green: 0, blue: 0, alpha: 0.5)
self.imageView = UIImageView.init(frame: CGRect.zero)
self.imageView.isUserInteractionEnabled = true
self.addSubview(self.imageView)
// 添加一个手势
let tapGestureRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(disMiss))
self.imageView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
2、 func waterfallZoom(initialFrame:CGRect ,image:UIImage ) -> Void 方法的实现
// MARK: 点击图像,原地放大显示图像
func waterfallZoom(initialFrame:CGRect ,image:UIImage ) -> Void {
// 设置图像在原始View上的位置
self.imageView.frame = initialFrame
self.imageView.image = image
// 创建图像变化后的宽与高的变量
var imageHeight :CGFloat
var imageWidth :CGFloat
// 判断图像是横向还是竖向的
if image.size.width / image.size.height > self.bounds.width / self.bounds.height {
// 判断图像的实际宽度与 self的实际宽度对比
if image.size.width > self.bounds.width {
// 获取现在的self 的中的高度
imageHeight = image.size.height / image.size.width * self.bounds.width
self.changeRect = CGRect.init(x: 0, y: (self.bounds.height - imageHeight)/2, width: self.bounds.width, height: imageHeight)
}else{
self.changeRect = CGRect.init(x: (self.bounds.width - image.size.width)/2, y: (self.bounds.height - image.size.height)/2, width: image.size.width, height: image.size.height)
}
} else{
// 判断图像的实际高度与self的实际高度对比
if image.size.height > self.bounds.height {
imageWidth = self.bounds.height * image.size.width / image.size.height
self.changeRect = CGRect.init(x: (self.bounds.width - imageWidth)/2, y: 0, width: imageWidth, height: self.bounds.height)
}else{
self.changeRect = CGRect.init(x: (self.bounds.width - image.size.width)/2, y: (self.bounds.height - image.size.height)/2, width: image.size.width, height: image.size.height)
}
}
// 大小变化的过程动画
UIView.animate(withDuration: 1.0) {
self.imageView.frame = self.changeRect
}
}
3 、手势方法的实现
// MARK: 清除
func disMiss() -> Void {
self.removeFromSuperview()
}