Swift瀑布流展示/切换简书列表数据
上篇文章介绍了HTML解析数据存入模型,今天我们的任务是将把之前拿到的数据可视化展示出来。因为组数据的的标题和摘要的文字多少不一,每个 item高度自然就不一样,我又想用collocationView展示两列数据,首先想到的就是瀑布流。现在很多移动端H5就是用瀑布流展示的,比如花瓣、Pinterest(需要梯子)。
2 - PinterestAPP布局
本文采用类似花瓣的布局给大家分享,先上一张最终效果图:
3 - 最终效果图
因为不管是OC还是Swift中UICollocationView的UICollectionViewFlowLayout默认都是只能设置统一的布局的,要做到如效果图一样的效果就需要重写UICollectionViewFlowLayout。
1 - 分析原理
首先我们迫切需要知道的是每个item高度是多少如何布局。上篇文章我们已经通过HTML解析到的数据计算出了高度,接下来就要通过制定一个规则,让每个item加在特定一列上。假如我们设定有两列数据,很显然我们不能通过列表数组的index的奇偶来确定它们的位置。因为假设数组arr的偶数下标值arr[i]的高度值都很大,奇数下标值arr[i]的高度值都很小,所以通过奇偶布局会出现第一列会比第2列高出很多,第二列下面会留很大的空白,这样肯定是不行。
4 - 按数组下标从左至右从上到下可能出现的情况
现在给出一种思路:数据数组findList传入自定义的Layout类,定义一个列高数组columnHeight记录每一列的总高度,然后定义一个记录每一列的总item个数的数组columnItemCount,最后定义一个元素类型为UICollectionViewLayoutAttributes的数组attributesArray此数组就是最终item布局的数组。遍历数据数组findList,拿到当前位置的IndexPath初始化一个UICollectionViewLayoutAttributes对象attributes,找到最短列,把数据追加在最短列,累加高度在columnHeight中的最短列,columnItemCount中最短列的个数+1,拿到前面的数据可以计算每个item的frame,最后把整个对象追加在布局数组attributesArray中。循环结束后,拿到最高列的高度减去最高列每个item间的间隙除以最高列的个数可以计算出平均值,这个值用来设置itemSize的高。最后把attributesArray赋值给self.layoutAttributesArray就可以了。如果要加头部和尾部就把头部尾部的布局数据插入头尾即可。大家可能看的很懵,直接上代码,注释很详细了 :
//
// FindFlowLayout.swift
// SwiftApp
//
// Created by leeson on 2018/7/4.
// Copyright © 2018年 李斯芃 ---> 512523045@qq.com. All rights reserved.
//
import UIKit
class HomeFlowLayout: UICollectionViewFlowLayout {
// 总列数
var columnCount:Int = 0
// 数据数组
var findList = [JianshuModel]()
// 整个webview的高度
private var maxH:Int?
// 头部高度
var headerH:CGFloat = 100
//所有item的属性
fileprivate var layoutAttributesArray = [UICollectionViewLayoutAttributes]()
override func prepare() {
let contentWidth:CGFloat = (self.collectionView?.bounds.size.width)! - self.sectionInset.left - self.sectionInset.right
let marginX = self.minimumInteritemSpacing
let itemWidth = (contentWidth - marginX * CGFloat(self.columnCount - 1)) / CGFloat.init(self.columnCount)
self.computeAttributesWithItemWidth(CGFloat(itemWidth))
}
///根据itemWidth计算布局属性
func computeAttributesWithItemWidth(_ itemWidth:CGFloat){
// 定义一个列高数组 记录每一列的总高度
var columnHeight = [Int](repeating: Int(self.sectionInset.top + self.headerH), count: self.columnCount)
// 定义一个记录每一列的总item个数的数组
var columnItemCount = [Int](repeating: 0, count: self.columnCount)
var attributesArray = [UICollectionViewLayoutAttributes]()
// 添加头部属性
let headerAttr:UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes.init(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: IndexPath.init(item: 0, section: 0))
headerAttr.frame = CGRect(x: 0, y: CGFloat(0), width: self.collectionView!.bounds.size.width, height: self.headerH)
attributesArray.append(headerAttr)
// 给属性数组设置数值
//self.layoutAttributesArray = attributesArray
// 遍历数据计算每个item的属性并布局
var index = 0
for data in self.findList {
let indexPath = IndexPath.init(item: index, section: 0)
let attributes = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
// 找出最短列号
let minHeight:Int = columnHeight.sorted().first!
let column = columnHeight.index(of: minHeight)
// 数据追加在最短列
columnItemCount[column!] += 1
let itemX = (itemWidth + self.minimumInteritemSpacing) * CGFloat(column!) + self.sectionInset.left
let itemY = minHeight
// 等比例缩放 计算item的高度
let itemH = Int(Double(data.itemHeight!)!)
// 设置frame
attributes.frame = CGRect(x: itemX, y: CGFloat(itemY), width: itemWidth, height: CGFloat(itemH))
attributesArray.append(attributes)
// 累加列高
columnHeight[column!] += itemH + Int(self.minimumLineSpacing)
index += 1
}
// 找出最高列列号
let maxHeight:Int = columnHeight.sorted().last!
let column = columnHeight.index(of: maxHeight)
// 根据最高列设置itemSize 使用总高度的平均值
let itemH = (maxHeight - Int(self.minimumLineSpacing) * (columnItemCount[column!] + 1)) / columnItemCount[column!]
self.itemSize = CGSize(width: itemWidth, height: CGFloat(itemH))
// 添加尾部属性
let footerIndexPath:IndexPath = IndexPath.init(item: 0, section: 0)
let footerAttr:UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes.init(forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, with: footerIndexPath)
footerAttr.frame = CGRect(x: 0, y: CGFloat(maxHeight), width: self.collectionView!.bounds.size.width, height: 30)
attributesArray.append(footerAttr)
// 给属性数组设置数值
self.layoutAttributesArray = attributesArray
self.maxH = maxHeight + 30
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return self.layoutAttributesArray
}
///重写设置contentSize,一定要写上这个方法,不然可能拉到最底部的时候可能会有很多空白
override var collectionViewContentSize: CGSize {
get {
return CGSize(width: (collectionView?.bounds.width)!, height: CGFloat(self.maxH!))
}
set {
self.collectionViewContentSize = newValue
}
}
}
5 - 按如上代码布局后的位置
2 - 数据展示
知道如何布局了就是常规的collocationView展示了,只是我这里加上了头部 尾部视图,还写了个简单的上下拉刷新。头部、尾部、cell我都是用xib写的,注册的时候记得用nib注册就行了。控制器里设置好自定layout的一些属性:
//MARK: - --- 设置item的布局
func setHomeFlowLayouts(){
//通过layout的一些参数设置item的宽度
let inset = UIEdgeInsetsMake(10, 10, 10, 10)
let minLine:CGFloat = 10.0
self.itemWidth = (SCREEN_WIDTH - inset.left - inset.right - minLine * (CGFloat(self.columnCount - 1))) / CGFloat(self.columnCount)
//设置布局属性
self.flowLayout.columnCount = self.columnCount
self.flowLayout.sectionInset = inset
self.flowLayout.minimumLineSpacing = minLine
}
还有就是调用网络请求的方法,这里是没有用到网络请求接口,是用的我上篇文章提到的HTML解析得到的数据模型,我在请求模型类JianshuRequestModel.swift里暴露了个方法,待解析完网页数据存入模型后用闭包(相当于OC的block)返回数组数据:
//网络请求回调
//参数:self.index是页码;self.itemWidth是透过计算得到的item的宽度
JianshuRequestModel.jianshuRequestDataWithPage(self.index, Float(self.itemWidth), { (headInfo) in
//返回headInfo头部信息,只有在page等于1的时候返回,因为每一页的头部信息都是一样的。
}) { (dataArr) in
//dataArr文章列表数据
}
至于上面提到的上下拉刷新,也就不赘述了,监听scrollView.contentOffset.y 和self.collectionView.contentInset设置个临界点做相应的操作即可,有疑惑的可以参考下文末的Demo。
3 - 切换布局
有运行过demo看过效果的同学可能会看到我在头部放了一个切换的按钮。此按钮的作用是用来切换布局的。默认情况下是两列,不停点击会在1列和2列中切换。self.columnCount这个成员变量就是设置列数的,理论上还可以设置3甚至更大的整数,不过这里我不推荐这么做,因为2列以上item的宽度会很窄,数据会挤在一起相当难看。
6 - 切换布局
我在头部视图类写了个按钮点击事件的闭包回调,在初始化头部视图的地方写上如下逻辑可切换布局:
//点击切换布局
self.headerView?.switchBack = { (click) in
print(click)
//切换列数
self.columnCount = (click == true) ? 1 : 2
self.setHomeFlowLayouts()
//遍历数组 重新计算高度
var num = 0
for model in self.dataArr {
//计算标题和摘要的高度
model.imgW = Float(self.itemWidth - 16)
model.imgH = model.wrap!.count > 0 ? model.imgW! * 120 / 150 : nil
model.titleH = GETSTRHEIGHT(fontSize: 20, width: CGFloat(model.imgW!) , words: model.title!) + 1
model.abstractH = GETSTRHEIGHT(fontSize: 14, width: CGFloat(model.imgW!) , words: model.abstract!) + 1
//item高度
var computeH:CGFloat = 8 + 25 + 3 + 10 + 8 + (model.imgH != nil ? CGFloat(model.imgH!) : 0) + 8 + model.titleH! + 8 + model.abstractH! + 8 + 10 + 8
//如果没有图片减去一个间隙8
computeH = computeH - (model.wrap!.count > 0 ? 0 : 8)
model.itemHeight = String(format: "%.f", computeH)
self.dataArr[num] = model;
num += 1
}
//重新赋值改变布局
self.flowLayout.findList = self.dataArr
//刷新视图
self.collectionView?.reloadData()
}
7 - 切换布局效果图
以上就是本文的全部内容,不懂的可以下载demo自行运行看下源码或者可以留言我。
下篇文章我将介绍文章详情的webview与js的交互,感兴趣的可以关注我,有更新会有提醒。
👉猛戳右侧链接下载 本文GitHub源码
上一篇文章:Swfit爬虫通过作者ID无接口获取简书文章列表,正则匹配HTML标签存储模型数据