最近项目中要实现一个动画效果,跟香奈儿首页很相似,多张图上下滑动,屏幕上面的图最大,点击下面的cell,点中的cell滚动到上面,并且放大显示。最终效果如下所示
1. 实现思路
这种能够上下滚动,显然是一个scrollview。我们可以通过自定义UICollectionViewFlowLayout,来实现我们想要的效果。
2. 自定义UICollectionViewFlowLayout
首先定义几个常量,方便后面代码中使用
// cell在下面的时候高度
let kNormalCellHeight: CGFloat = kScreenWidth * 0.3
// cell在屏幕上面第一个时候高度
let kBigCellHeight: CGFloat = kScreenWidth * 1.0
let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height
let kRectRange: CGFloat = UIScreen.main.bounds.size.height + kNormalCellHeight * 2
设置collectionViewContentSize,注意凡是滑到屏幕上方的cell高度都是kBigCellHeight,所以为了最后一个cell显示的下,contentSize进行如下设置
override var collectionViewContentSize: CGSize {
return CGSize(width: kScreenWidth, height: kBigCellHeight * CGFloat(count) + (kScreenHeight - kBigCellHeight))
}
初始化一些变量,默认的cell高度就是kNormalCellHeight
override init() {
super.init()
itemSize = CGSize(width: kScreenWidth, height: kNormalCellHeight)
scrollDirection = .vertical
minimumInteritemSpacing = 0
minimumLineSpacing = 0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
重写prepare和shouldInvalidateLayout
// 在布局开始的时候,layout对象会先调用prepareLayout方法,这个方法里面你可以计算一会儿你要用到的信息。 prepareLayout方法并不是必须实现的,但是它给你一个机会去做一些必要地初始化计算。
override func prepare() {
super.prepare()
}
// 当前layout的布局发生变动时,是否重写加载该layout。默认返回NO,若返回YES,则重新执行prepare和layoutAttributesForElements(in rect: CGRect)
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
重写layoutAttributesForElements(in rect: CGRect),这个方法是核心,返回在collectionView的可见范围内(bounds)所有item对应的layoutAttrure对象装成的数组。collectionView的每个item都对应一个专门的UICollectionViewLayoutAttributes类型的对象来表示该item的一些属性,比如bounds, size, transform, alpha等。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let screen_y = collectionView?.contentOffset.y
let current_floor = floorf(Float((screen_y!) / kBigCellHeight)) + 1
let current_mod = fmodf(Float(screen_y!), Float(kBigCellHeight))
let percent = current_mod / Float(kBigCellHeight)
var correctRect: CGRect = .zero
if current_floor == 0 || current_floor == 1 {
correctRect = CGRect(x: 0, y: 0, width: kScreenWidth, height: kRectRange)
} else {
correctRect = CGRect(x: 0, y: kNormalCellHeight * CGFloat(current_floor - 2), width: kScreenWidth, height: kRectRange)
}
let original = super.layoutAttributesForElements(in: correctRect)
let array = original
let incrementalHeightOfCurrentItem = kBigCellHeight - kNormalCellHeight
if screen_y! >= 0 {
for attributes in array! {
let row = attributes.indexPath.row
if row < Int(current_floor) {
// zIndex 表示层级,数字越大,层级越高(最上面)
attributes.zIndex = 7
attributes.frame = CGRect(x: 0, y: kBigCellHeight * CGFloat(row - 1), width: kScreenWidth, height: kBigCellHeight)
} else if (row == Int(current_floor)) {
attributes.zIndex = 8
attributes.frame = CGRect(x: 0, y: kBigCellHeight * CGFloat(row - 1), width: kScreenWidth, height: kBigCellHeight)
} else if (row == Int(current_floor) + 1) {
attributes.zIndex = 9
let part = (CGFloat(current_floor) - 1) * incrementalHeightOfCurrentItem
let partOne = attributes.frame.origin.y + part
let partTwo = kNormalCellHeight + (kBigCellHeight - kNormalCellHeight) * CGFloat(percent)
attributes.frame = CGRect(x: 0, y: partOne, width: kScreenWidth, height: partTwo)
} else {
if row == Int(current_floor) + 2 {
attributes.zIndex = 6
} else if (row == Int(current_floor) + 3) {
attributes.zIndex = 5
} else if (row == Int(current_floor) + 4) {
attributes.zIndex = 4
} else if (row == Int(current_floor) + 5) {
attributes.zIndex = 3
} else if (row == Int(current_floor) + 6) {
attributes.zIndex = 2
} else if (row == Int(current_floor) + 7) {
attributes.zIndex = 1
} else {
attributes.zIndex = 0
}
let partOne = (current_floor - 1) * Float(incrementalHeightOfCurrentItem)
let originY = Float(attributes.frame.origin.y) + partOne + Float(incrementalHeightOfCurrentItem) * percent
attributes.frame = CGRect(x: 0, y: CGFloat(originY), width: kScreenWidth, height: kNormalCellHeight)
}
}
}
return array
}
重写targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint),返回layout“最终”的偏移量,何谓“最终”,手指离开屏幕时layout的偏移量不是最终的,因为它有惯性,当它停止时才是“最终”偏移量。
注意:
- 整个方法都是在求手指离开屏幕后滑动到的最终点,因为是上下滑,所以主要是找到Y值
- 如果手指滑动后停下来再离开,那么velocity.y == 0,如果手指向上滑动,contentOffset.y变大,那么velocity.y > 0,如果手指向下滑动,contentOffset.y变小,那么velocity.y < 0
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// 整个方法所有的核心都是在求这个destinationPointY
var destinationPointY: CGFloat
var destinationPoint: CGPoint = .zero
let contentOffsetY: CGFloat = (self.collectionView?.contentOffset.y)!
var realVelocityY: CGFloat
var cellLocation: CGFloat
if contentOffsetY < 0 {
return proposedContentOffset
}
if velocity.y == 0 {
cellLocation = CGFloat(roundf(Float((proposedContentOffset.y)/kBigCellHeight))) + 1
self.currentCount = Int(cellLocation)
if cellLocation == 0 {
destinationPointY = 0
} else {
destinationPointY = (cellLocation - 1) * kBigCellHeight
}
} else {
if velocity.y > 1 {
realVelocityY = 1
} else if (velocity.y < -1) {
realVelocityY = -1
} else {
realVelocityY = velocity.y
}
if velocity.y > 0 {
cellLocation = CGFloat(ceilf(Float((contentOffsetY + realVelocityY * kBigCellHeight) / kBigCellHeight)) + 1)
} else {
cellLocation = CGFloat(floorf(Float((contentOffsetY + realVelocityY * kBigCellHeight) / kBigCellHeight)) + 1)
}
if cellLocation == 0 {
destinationPointY = 0
currentCount = 1
} else {
if velocity.y > 0 {
cellLocation = CGFloat(self.currentCount + 1)
self.currentCount = self.currentCount + 1
} else {
cellLocation = CGFloat(self.currentCount - 1)
self.currentCount = self.currentCount - 1
}
destinationPointY = (cellLocation - 1) * kBigCellHeight
}
}
if destinationPointY < 0 {
destinationPointY = 0
}
if destinationPointY > ((self.collectionView?.contentSize.height)! - kScreenHeight) {
destinationPointY = ((self.collectionView?.contentSize.height)! - kScreenHeight)
self.currentCount = self.currentCount - 1
cellLocation = CGFloat(self.currentCount)
}
self.collectionView?.decelerationRate = 0.1
destinationPoint = CGPoint(x: 0, y: destinationPointY)
return destinationPoint
}
3. 点击cell时候的处理
如果是顶上的大图则不处理,如果是下面的图则滚动到指定位置即可
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let offset = ceilf(Float(kBigCellHeight) * Float(indexPath.row - 1))
if ceilf(Float(collectionView.contentOffset.y)) != offset {
self.layout?.currentCount = indexPath.row
collectionView.setContentOffset(CGPoint.init(x: 0, y: Int(offset)), animated: true)
} else {
print("点击了大图哦")
}
print("点击了第\(indexPath.row)个")
}