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

Posted HelloWord杰少

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift 自定义布局实现 Cover Flow 效果相关的知识,希望对你有一定的参考价值。

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

写在开头

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

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

思路分析

闲话少说,直接进入正题,通过上面的效果图,我们可以分析到得出 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
    



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

写在结尾

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

https://github.com/ShenJieSuzhou/SwiftScrollBanner

相关阅读:

UICollectionView 自定义布局实现瀑布流视图
使用 UICollectionView 实现分页滑动效果
使用 UICollectionView 实现首页卡片轮播效果

关注我的技术公众号,获取优质技术文章。
微信扫一扫下方二维码即可关注:

以上是关于Swift 自定义布局实现 Cover Flow 效果的主要内容,如果未能解决你的问题,请参考以下文章

iOS 中的自定义 Cover Flow 实现

Collection View 自定义布局(custom flow layout)

使用 Swift 3 在 UICollectionView 上自定义布局

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

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

提交前 Swift segue 自定义转换布局