Swift 自定义布局实现 Cover Flow 效果

3,325 阅读6分钟

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 HelloWorld杰少 即可关注。

写在开头

大家早上好,今天我又给大家带来了一篇关于 UICollectionView 系列的文章,在上一篇文章中,我们实现了一个酷炫的瀑布流布局,带大家初步的了解了在 UICollectionView 中该如何创建自定义布局。但是上一篇中实现的自定义布局稍显简单,只能说是比较粗略的计算了下布局各个 item 的位置,搞明白了继承自 UICollectionFlowLayout 子类它需要重载的方法的意义,那么今天这篇文章我们就来实现一个更加复杂的自定义布局: Cover Flow 效果吧!

首先大先看下 Cover Flow 的效果图,如下:

image

思路分析

闲话少说,直接进入正题,通过上面的效果图,我们可以分析到得出 Cover Flow 布局具有以下这些特性:

  • UICollectionView 的滚动方向是横向的

  • 随着 UICollectionView 滚动,Cell 会自动的进行缩放,当 Cell 的中心点与 UICollectionView 的中心点重合时放大,偏离中心点时缩小

  • Cell 的滚动是分页滚动,而且每次停止的位置都是与UICollectionView 的中心点重合

需求已经明确了,那我们该如何去实现呢!

首先,要实现 UICollectionView 只支持横向滚动,很简单,仅需要设置 UICollectionFlowLayout 布局对象中的 scrollDirection 为 horizontal 即可.

第二步,要实现 Cell 随 UICollectionView 滚动时具有缩放效果,就需要找一个合适的时机对 Cell 进行缩放,我的思路是先计算出 UICollectionView 整体滚动内容的中心点的 x 坐标,然后遍历每一个 Cell 的布局,找出它的中心点 x 坐标,并计算这俩个 x 坐标的偏移值,俩者的距离越小,缩放比越小,反之则越大,我这边设定缩放比最大为 1,当俩者的 x 坐标重合时,也就是没有偏移值的时候,缩放比就为 1.

第三步,实现 Cell 的滚动是分页带阻尼的效果,并且滑动停止的时候当前放大的 Cell 居中显示,有的同学会说:UICollectionView 自带了分页效果,只需要设置 isPagingEnabled 为 true,不就可以实现分页了吗?同学你讲的没错,但是当我们 Cell 的 width 加上边距等如果不占满 UICollectionView,那么就会出现一个问题,虽然你实现了分页效果,但是你的 Cell 在滚动的过程中是不会居中的. 那该如何不通过设置 isPagingEnabled 来实现 Cell 分页滚动和居中显示呢!请接着往下看.

读过我前几篇 UICollectionView 系列的小伙伴们,不知道你们还有没有印象,我写过一篇教程叫做 "使用 UICollectionView 实现分页滑动效果" 这里附上链接(),里面讲述的就是如何不通过设置 isPagingEnabled 来实现分页效果,在里面我提到了一个很重要的方法叫做:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint

它的作用在于 UICollectionView 停止滚动时,返回一个新的偏移点坐标,它有俩个参数,第一个参数 proposedContentOffset 指的是滚动将要停止时的偏移点坐标,第二个参数 velocity 指的是滚动速度;那既然我们能获取到当前滚动即将停止的坐标,那我们就可以修改它,使它的新的偏移点坐标能让 Cell 居中显示,在这里就不做更多的阐述了,直接浏览下方的代码吧!

逻辑实现

Talk is cheap, show me the code, 下面就呈上 Cover Flow 布局的源码供大家参考,里面一些涉及到计算的逻辑,我已经用注释写明,代码如下:

//
//  CoverFlowLayout.swift
//  SwiftScrollBanner
//
//  Created by shenjie on 2021/2/24.
//

import UIKit

class CoverFlowLayout: UICollectionViewFlowLayout {
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // 1.获取该范围内的布局数组
        let attributes = super.layoutAttributesForElements(in: rect)
        // 2.计算出整体中心点的 x 坐标
        let centerX = collectionView!.contentOffset.x + collectionView!.bounds.width / 2
        
        // 3.根据当前的滚动,对每个 cell 进行相应的缩放
        attributes?.forEach({ (attr) in
            // 获取每个 cell 的中心点,并计算这俩个中心点的偏移值
            let pad = abs(centerX - attr.center.x)
            
            // 如何计算缩放比?我的思路是,距离越小,缩放比越小,缩放比最大是1,当俩个中心点的 x 坐标
            // 重合的时候,缩放比就为 1.
            
            // 缩放因子
            let factor = 0.0009
            // 计算缩放比
            let scale = 1 / (1 + pad * CGFloat(factor))
            attr.transform = CGAffineTransform(scaleX: scale, y: scale)
        })
        // 4.返回修改后的 attributes 数组
        return attributes
    }
        
    /// 滚动时停下的偏移量
    /// - Parameters:
    ///   - proposedContentOffset: 将要停止的点
    ///   - velocity: 滚动速度
    /// - Returns: 滚动停止的点
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var targetPoint = proposedContentOffset
        // 1.计算中心点的 x 值
        let centerX = proposedContentOffset.x + collectionView!.bounds.width / 2
        // 2.获取这个点可视范围内的布局属性
        let attrs = self.layoutAttributesForElements(in: CGRect(x: proposedContentOffset.x, y: proposedContentOffset.y, width: collectionView!.bounds.size.width, height: collectionView!.bounds.size.height))
        
        // 3. 需要移动的最小距离
        var moveDistance: CGFloat = CGFloat(MAXFLOAT)
        // 4.遍历数组找出最小距离
        attrs!.forEach { (attr) in
            if abs(attr.center.x - centerX) < abs(moveDistance) {
                moveDistance = attr.center.x - centerX
            }
        }
        // 5.返回一个新的偏移点
        if targetPoint.x > 0 && targetPoint.x < collectionViewContentSize.width - collectionView!.bounds.width {
            targetPoint.x += moveDistance
        }
        
        return targetPoint
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: sectionInset.left + sectionInset.right + (CGFloat(collectionView!.numberOfItems(inSection: 0)) * (itemSize.width + minimumLineSpacing)) - minimumLineSpacing, height: 0)
    }
}

衔接 UIViewController

Cover Flow 的自定义布局已经实现好了,那剩下的就是在视图控制器中呈现了,这一步实现起来很简单,也不做赘述了,直接看源码:

//
//  CoverFlowViewController.swift
//  SwiftScrollBanner
//
//  Created by shenjie on 2021/2/23.
//

import UIKit

class CoverFlowViewController: UIViewController {

    private let cellID = "baseCellID"
    var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setUpView()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    }
    
    func setUpView() {
        // 初始化 flowlayout
        let layout = CoverFlowLayout()
        let margin: CGFloat = 20
        let collH: CGFloat = 200
        let itemH = collH - margin * 2
        let itemW = view.bounds.width - margin * 2 - 100
        layout.itemSize = CGSize(width: itemW, height: itemH)
        layout.minimumLineSpacing = 5
        layout.minimumInteritemSpacing = 5
        layout.sectionInset = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
        layout.scrollDirection = .horizontal
        
        // 初始化 collectionview
        collectionView = UICollectionView(frame: CGRect(x: 0, y: 180, width: view.bounds.width, height: collH), collectionViewLayout: layout)
        collectionView.backgroundColor = .black
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.dataSource = self
        collectionView.delegate = self
        
        // 注册 Cell
        collectionView.register(BaseCollectionViewCell.self, forCellWithReuseIdentifier: cellID)
        view.addSubview(collectionView)
    }
}

extension CoverFlowViewController: UICollectionViewDelegate{
    func scrollViewDidScroll(_ scrollView: UIScrollView) {

    }
}

extension CoverFlowViewController: UICollectionViewDataSource{
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 15
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! BaseCollectionViewCell
        cell.cellIndex = indexPath.item
        cell.backgroundColor = indexPath.item % 2 == 0 ? .purple : .red

        return cell
    }
}


编译运行后的效果如图所示:

image

写在结尾

好了,本篇教程到这里就结束了,这篇文章是 UICollectionView 教程系列的第四篇,接下来我还会继续更新;如果大家有什么疑问,可以通过我的公号与我交流,也欢迎大家来纠错,老样子最后附上项目工程地址:

github.com/ShenJieSuzh…

相关阅读:

UICollectionView 自定义布局实现瀑布流视图

使用 UICollectionView 实现分页滑动效果

使用 UICollectionView 实现首页卡片轮播效果

关注我的技术公众号"HelloWorld杰少",获取更多优质技术文章。