iOS UICollectionView 横向分页布局

本文介绍了如何自定义UICollectionView实现横向分页布局,通过创建一个继承自UICollectionViewFlowLayout的类,设置分页属性并调整item的x、y坐标。在布局过程中,作者遇到并解决了分页处item位置不正确的问题,通过计算间隔实现正确偏移,最终实现预期效果。
部署运行你感兴趣的模型镜像

有一种流行的 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

githubDemo

这是我的一些思考方式,如果您有更好的方法、或看到写的不好的地方,欢迎留言探讨。

转载于:https://juejin.im/post/5b47509ef265da0f652373b9

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值