Swift 自定义布局实现 Cover Flow 效果
写在开头
大家早上好,今天我又给大家带来了一篇关于 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 教程系列的第四篇,接下来我还会继续更新;如果大家有什么疑问,可以通过我的公号与我交流,也欢迎大家来纠错,老样子最后附上项目工程地址:
- 点赞
- 收藏
- 关注作者
评论(0)