github源碼地址
效果展示
前言
去年因為項目中有個切換學(xué)校的功能,要求以卡片浮動效果展示,并且能夠無限循環(huán)滾動。
之前找了個demo它是通過自定義view動畫實現(xiàn)的,卡片數(shù)量多的時候會比較卡頓,所以研究了一下,自己造了一個,現(xiàn)在把實現(xiàn)的思路分享一下~
好,開始正題~~~
技術(shù)調(diào)研
在實現(xiàn)之前,首先我想到的是如何實現(xiàn)無限循環(huán)的問題,以及多數(shù)量時如何復(fù)用的問題。關(guān)于view
的無限循環(huán)滾動和復(fù)用問題,最經(jīng)典的就是輪播圖了,現(xiàn)在比較常用的兩種方法就是UIScrollView
或者UICollectionView
實現(xiàn)的。
然后我決定選用UICollectionView
,因為我們只需要給它提供數(shù)據(jù)源驅(qū)動,它自己就可以復(fù)用,實現(xiàn)應(yīng)該比較簡單。并且UICollectionView
的layout
的靈活性非常強,完全可以自定義出很多酷炫的效果。
橫向滾動
設(shè)置滾動方向為水平,禁用分頁屬性(默認(rèn)就是false),這樣就可以橫向流暢滑動了
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
collectionView.isPagingEnabled = false // default NO
無限循環(huán)
定義數(shù)組imageArr
來存儲圖片作為數(shù)據(jù)源,在每次滾動結(jié)束后,都重新定位到第一張。
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at:.centeredHorizontally, animated: false)
}
但是每次重定位的時候都會出現(xiàn)明顯的突兀效果,所以我們可以把這個數(shù)組中的圖片復(fù)制100份、200份、1000份。。。這樣就有足夠多的數(shù)據(jù)來供用戶滑動期間來展示了。
但是直接存儲圖片的話,這個數(shù)組肯定會占用很大的內(nèi)存,所以我們開辟一個新的數(shù)組indexArr
,來存儲imageArr
中圖片的下標(biāo),因為只是存儲的是int,所以占用的內(nèi)存是非常小的。
// 初始化數(shù)據(jù)源
imageArr = ["num_1", "num_2", "num_3", "num_4", "num_5"]
for _ in 0 ..< 100 {
for j in 0 ..< imageArr.count {
indexArr.append(j)
}
}
比如imageArr中是["pic1", "pic2", "pic3"],那么indexArr中就是[0,1,2,0,1,2,0,1,2...],這樣就可以把數(shù)據(jù)源設(shè)置為indexArr。
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return indexArr.count
}
代碼初始時我們把collectionView定位到中間那組(第50組,假設(shè)共設(shè)置了100組),每次滾動結(jié)束后也再次定位到中間組的第N張(N:上次滑動結(jié)束時的那張)。
// 定位到 第50組(中間那組)
override func viewDidLoad() {
super.viewDidLoad()
collectionView.scrollToItem(at: IndexPath(item: 50 * imageArr.count, section: 0) , at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
}
每次動畫停止時,也重新定位到 第50組(中間那組) 模型
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
collectionView.scrollToItem(at: IndexPath(item: 50 * imageArr.count, section: 0) , at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
}
每張卡片(cell)的數(shù)據(jù)填充
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(CyclicCardCell.self), for: indexPath) as! CyclicCardCell
let index = indexArr[indexPath.row]
cell.index = index
cell.cardImgView.image = UIImage(named: imageArr[index])
cell.cardNameLabel.text = "奔跑吧,小蝸牛~"
return cell
}
滾動動畫的平滑過渡
在每次重定位的時候,雖然設(shè)置的是回到中間組的對應(yīng)下標(biāo)的那個cell,并且animated也是設(shè)置的false,但是依然可以看出動畫有些不連貫、突兀,顯得不流暢自然。
這就需要我們自定義UICollectionViewFlowLayout,來重寫targetContentOffset方法,通過遍歷目標(biāo)區(qū)域中的cell,來計算出距離中心點最近的cell,把它調(diào)整到中間,實現(xiàn)平緩流暢的滑動結(jié)束的效果。
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
// 目標(biāo)區(qū)域中包含的cell
let attriArray = super.layoutAttributesForElements(in: targetRect)! as [UICollectionViewLayoutAttributes]
// collectionView落在屏幕中點的x坐標(biāo)
let horizontalCenterX = proposedContentOffset.x + (self.collectionView!.bounds.width / 2.0)
var offsetAdjustment = CGFloat(MAXFLOAT)
for layoutAttributes in attriArray {
let itemHorizontalCenterX = layoutAttributes.center.x
// 找出離中心點最近的
if(abs(itemHorizontalCenterX-horizontalCenterX) < abs(offsetAdjustment)){
offsetAdjustment = itemHorizontalCenterX-horizontalCenterX
}
}
//返回collectionView最終停留的位置
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
卡片的縮放動畫
在自定義的UICollectionViewFlowLayout中重寫layoutAttributesForElements方法,通過該方法,可以獲取到可視范圍內(nèi)的cell。
然后在cell的滑動過程中,通過cell偏移的距離來進行尺寸的縮放系數(shù)的設(shè)置,處于最中心的系數(shù)為1,則為原本的大小,隨著中心距離的偏移,系數(shù)會逐漸變小為0.98,0.95,0.8...
因此卡片也會隨之變小,從而達到在滾動過程中,滾動至中間的卡片最大,旁邊的變小的效果。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let array = super.layoutAttributesForElements(in: rect)
var visibleRect = CGRect()
visibleRect.origin = self.collectionView!.contentOffset
visibleRect.size = self.collectionView!.bounds.size
for attributes in array!{
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = abs(distance/ActiveDistance)
let zoom = 1 - ScaleFactor * normalizedDistance
attributes.transform3D = CATransform3DMakeScale(1.0, zoom, 1.0)
attributes.zIndex = 1
// let alpha = 1 - normalizedDistance
// attributes.alpha = alpha
}
return array
}
OK,至此主要的核心實現(xiàn)就完成了,可能有些地方思路表述的可能不是太清楚,大家可以去github上下載源碼看下,有不對的地方,歡迎指正~
9月28號更新:
因為這個文章是第一次寫的,所以之前表述的不大好,今天重新修改了一下,希望能對大家有所幫助??
剛寫了一個類似的重疊卡片滾動的動畫,有興趣的可以看下: