有一种流行的 UICollectionView 横向分页布局方式,当你布局好 itemSize 等信息后,兴冲冲的运行模拟器时,却发现结果是这样的:
此时你想要的布局方式应该是这样:
那我们就需要重新自定义 UICollectionViewFlowLayout。
网上看了一些写法,不过大部分都是相同的,由于看不懂(智商太低了),而且网上的写法没能解决我后面的一个问题(后面会说到,分页处的 item x 不对的问题),于是自己思考写了一个。不足之处,还望指正。
重新布局
新建一个继承于 UICollectionViewFlowLayout 的类,定义好基本信息:
self.collectionView?.isPagingEnabled = true
复制代码
final class ItemFlowLayout: UICollectionViewFlowLayout {
/// 包含了所有重新布局的 item,在代理方法中,需要返回这个我们自己包装的数组。
private lazy var allAttrs = [UICollectionViewLayoutAttributes]()
/// 图片大小
private lazy var iconSize = #imageLiteral(resourceName: "惠整形").size
/// item 间隔
private var space: CGFloat {
return (collectionView!.frame.width - iconSize.width * 5) / 6
}
/// 设置分页大小
override var collectionViewContentSize: CGSize {
return CGSize(width: screenWidth * 2, height: iconSize.height + 17 + 5)
}
override init() {
super.init()
itemSize = CGSize(width: iconSize.width, height: iconSize.height + 17 + 5)
scrollDirection = .horizontal
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
复制代码
已就绪,此时需要思考的就是上面的两种布局方式应该如何转换过来,而且还能开启分页模式,不过开启分页模式比较简单,在上面的代码中,我已经重写了一个系统属性:
/// 设置分页大小
override var collectionViewContentSize: CGSize {
return CGSize(width: screenWidth * 2, height: iconSize.height + 17 + 5)
}
复制代码
这是个只读属性,只能用重写的方式设置。设置后再次运行就发现可以开启分页模式了。
思想很重要
每一种模式的背后都有其核心思想,这个布局方式就是要我们思考他背后的原理,想一下他整体的大概布局方式,要怎么样才能做到,只要有一点灵感就试一下,我也是慢慢试了才成功的。
这个布局我的想法是:
既然每一个分页有 10 个 item ( perRowCount * rowCount ),那我就把每一个分页的所有 item 都遍历出来,然后再一一为其设置 x, y 就好了:
// CollectionView 准备布局时调用
override func prepare() {
super.prepare()
// 每行显示的 item 个数
let perRowCount = 5
// 每个分页有几行
let rowCount = 2
// 遍历一个组的所有 item
(0..<self.collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍历出每个分页的 item 索引
let index = item % (perRowCount * rowCount)
// 在 0 -> 9 (perRowCount * rowCount) 之间循环
// 输出: 0, 1, 2... 0,1
// 正好对应上我们每一个分页的 item 数量,有了这个想法之后,
// 我就想,既然这样,那我就 < 5 的时候布局上面的,否则就布局下面的,不就行了吗?
print(index)
// 取出默认的 item 布局信息。
// attrs 有 frame 属性,可以修改其 x, y 重新布局。
let attrs = self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))!
self.allAttrs.append(attrs)
}
}
/// 返回当前可以显示的 item,
/// 该方法会多次调用,所以不能把添加数组的代码写在这里。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return allAttrs.filter { rect.contains($0.frame) }
}
复制代码
布局 x, y
// 为了方便,我创建一个临时索引来计算 x 的值
var z = 0
(0..<collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍历出每个分页的 item 索引
let index = item % (perRowCount * rowCount)
let attrs = self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))!
if index < perRowCount { /// 设置上面的 origin
// 没什么好说的,y 的话都是 space
// 相当于 inset.top
attrs.frame.origin.y = space
// 如果不懂的话我们慢慢解释一下:
// space + itemSize.width * CGFloat(z)
// 如果只是这样的话相信你还是能看得懂的,我们在循环里经常这样写,
// 这样的话除了开始处的 x 有间隔外,其余 item 都是紧紧贴着的,
// 我们在其后面加上: space * CGFloat(z) 来分开。
// 这样的话就每个 item 之间都有间隔了。
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z)
z += 1
}else { /// 设置下面 item 的 origin
// 这里我本来也想像上面那样创建一个临时变量来计算 x,
// 但是发现这种写法正好得出下面的 index
// 其结果是: 0, 1, 2...
let index = item - z
// 隔两个间距的高度和本身的高,正好得出下面 y 的所在位置。
attrs.frame.origin.y = space * 2 + itemSize.height
// x 和上面一样,一直乘上去就好了。
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index)
}
self.allAttrs.append(attrs)
}
复制代码
这样基本就好了,但是你运行后会发现有问题(如果你有多余的 item 的话),就像这样:
你可能碰上了类似这样的事情,后面分页的 x 不对了,没有和开始处间隔开来。相信是什么问题你应该知道了。就是我们前面设置了 x 的问题,我们忽略了分页后的 x,此时也应该也要偏移一下的。要怎么办呢?
通过思考有了一个想法:
既然每个 item 之间通过 + space * CGFloat(z) 之后能够计算出其间隔,那么是不是也可以通过 +..curpage * xx 之类的计算出其分页的间隔呢?
于是试了一下,发现这样的代码可行:
var curpage = 0
(0..<collectionView!.numberOfItems(inSection: 0)).forEach { (item) in
// 遍历出每个分页的 item 索引
let index = item % (perRowCount * rowCount)
// 得出当前所在分页
// 忽略掉第 0 页,并且在每一次 index 等于 0 时,计算出当前所在的是第几分页.
// 因为 index 每一次等于 0,都是一次新的分页。
if item != 0 && index == 0 { curpage += 1 }
if index < perRowCount {
// 在前面追加: `+ space * CGFloat(curapge)`
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z + curpage) + space * CGFloat(curapge)
}else {
let index = item - z
// 在前面追加: `+ space * CGFloat(curapge)`
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index + curpage) + space * CGFloat(curapge)
}
}
复制代码
这样就算好了,再次运行模拟器发现每个 item 的偏移应该是正确的。
为了简单,我们可以再简化下赋值 x 的写法:
if index < perRowCount {
attrs.frame.origin.y = space
// 后面改成: `+ space * CGFloat(z + curpage)`
// 其结果是一样的。
attrs.frame.origin.x = space + itemSize.width * CGFloat(z) + space * CGFloat(z + curpage)
z += 1
}else {
let index = item - z
attrs.frame.origin.y = space * 2 + itemSize.height
attrs.frame.origin.x = space + itemSize.width * CGFloat(index) + space * CGFloat(index + curpage)
}
复制代码
效果
完整 Demo
这是我的一些思考方式,如果您有更好的方法、或看到写的不好的地方,欢迎留言探讨。
本文介绍了如何自定义UICollectionView实现横向分页布局,通过创建一个继承自UICollectionViewFlowLayout的类,设置分页属性并调整item的x、y坐标。在布局过程中,作者遇到并解决了分页处item位置不正确的问题,通过计算间隔实现正确偏移,最终实现预期效果。
2757

被折叠的 条评论
为什么被折叠?



