上一篇介绍了UICollectionView的基本使用,这篇介绍下如何实现如Keep勋章的滑动自动缩放的交互效果,即Gallery画廊效果,只需要利用layout即可轻松实现。
Keep勋章
我们先按上一篇创建UICollectionView以显示数据,代码如下:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 100, height: 100)
let collectionView = UICollectionView(frame: CGRectMake(0, 100, view.bounds.width, 150), collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.register(UINib(nibName: "ItemCell", bundle: nil), forCellWithReuseIdentifier: "ItemCell")
view.addSubview(collectionView)
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 30
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ItemCell", for: indexPath) as! ItemCell
cell.imageView.image = UIImage(named: "\(indexPath.row)")
return cell
}
}
因为我们还是像上一篇一样用的UICollectionViewFlowLayout,运行后,目前只是简单的平铺效果:
Simulator Screenshot - iPhone 15 - 2023-12-20 at 17.47.47.png
接下来我们要替换layout为我们自己创建的UICollectionViewLayout子类。因为总体上我们还是网格布局,可以直接继承自UICollectionViewFlowLayout,可节省大量代码:
创建LineLayout
先上完整代码,再解释
import UIKit
class LineLayout: UICollectionViewFlowLayout {
let ActiveDistance = 80.0;
let ScaleFactor = 1.0;
var seletedIndex = 0
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 20
itemSize = CGSize(width: 110, height: 110)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 调整选中的cell居中对齐
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
//proposedContentOffset是没有设置对齐时本应该停下的位置
guard let collectionView = collectionView else {return proposedContentOffset}
//targetRect是collectionView当前显示在屏幕的区域,array保存此区域内的所有cell布局信息(UICollectionViewLayoutAttributes)
let collectionW = collectionView.bounds.width
let collectionH = collectionView.bounds.height
var offsetAdjustment = 10000.0
let horizontalCenter = proposedContentOffset.x + collectionW / 2
let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: collectionW, height: collectionH)
guard let array = super.layoutAttributesForElements(in: targetRect) else {return proposedContentOffset}
//对当前屏幕中的UICollectionViewLayoutAttributes逐个与屏幕中心进行比较,找出最接近中心的一个
var itemHorizontalCenter = 0.0
for layoutAttributes in array {
itemHorizontalCenter = layoutAttributes.center.x
if (abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
//返回调整后的偏移位置
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y
)
}
/** 有效距离:当item的中间x距离屏幕的中间x在ActiveDistance以内,开始放大, 其它情况都是缩小 */
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let array = super.layoutAttributesForElements(in: rect)
guard let collectionView = collectionView,let array = array else {return array}
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
// 遍历布局属性
for attribute in array {
//如果cell在屏幕上则进行缩放处理
if CGRectIntersectsRect(attribute.frame, rect) {
// 距离中点的距离
let distance = CGRectGetMidX(visibleRect) - attribute.center.x
let normalizedDistance = distance / ActiveDistance
if (abs(distance) < ActiveDistance) {
let zoom = 1 + ScaleFactor * (1 - abs(normalizedDistance))
attribute.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0)
attribute.zIndex = 1
attribute.alpha = max(1 - normalizedDistance, 0.6)
seletedIndex = attribute.indexPath.row
}else {
attribute.alpha = 0.6
}
}
}
return array
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
true
}
}
1、init方法
在init方法中,我们设置了collectionView的滑动方向、cell间距、cell的大小
2、实现滑动后cell自动居中
通过重写如下方法:
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
其中proposedContentOffset参数为滑动结束后,collectionView最终的偏移位置,返回值为我们期待的偏移位置。即我们只要通过本该偏移的位置,通过计算找到此时离屏幕中点最近的cell,重新计算偏移位置并返回,就可以让最靠近中点的cell完全居中了。
3、中间的cell放大效果
通过重写以下方法:
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
参数rect为当前需要展示的所有cell所在collectionview的区域,我们只需要获取到这区域内的所有cell的布局信息,并做对应的缩放、透明度的修改后,作为返回值返回即可。
最后,将原来的UICollectionViewFlowLayout替换为LineLayout即可:
override func viewDidLoad() {
super.viewDidLoad()
let layout = LineLayout()
let collectionView = UICollectionView(frame: CGRectMake(0, 100, view.bounds.width, 150), collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.register(UINib(nibName: "ItemCell", bundle: nil), forCellWithReuseIdentifier: "ItemCell")
view.addSubview(collectionView)
}
运行后效果如下:
最终效果