作者:@翁呀偉呀 授權本站轉載。
這次的示例是我看過了 這篇Blog 後自己實現的。那篇 blog 裡只寫了個開頭,後邊的內容好像沒時間寫,但是我實現後感覺有很多問題。所以貼到這裡,希望有人能指導一下。
ps: 題目沒有別的意思,只是單純覺得這3個字放在這裡特別和諧,真的,我反正是信了。
效果圖
首先看要實現的效果:

然後看看我實現的效果:

分析
包含若干個子視圖,每層子視圖越往後,寬度越小,y值越小,不透明度越小,逐層遞減。
需要創建 2 個手勢,一點單擊手勢 Tap,一個滑動手勢 Pan。
滑動的時候,每層卡片都要往某個方向移動,並且每層卡片移動的距離也要遞減。
滑動的時候,還需要旋轉,並且也是逐層遞減的。
滑動超過一個距離後,第一張卡片移除屏幕,其他的卡片依次先前移動。
單擊的時候,需要翻轉第一張視圖。
激動人心的代碼部分
創建卡片
這些卡片,我采用 UIImageView 代替,也就是說首先創建若干個 imageView。
為了重構方便,我將創建卡片分為 3 個方法,依次是:
這個方法用來 初始化一個卡片,傳入卡片和索引,就會初始化它的 y方向上的距離、橫向的縮放、不透明度的遞減。
func setUpImageView(imageView: UIImageView, index: Int) {
var transform = CATransform3DIdentity
transform.m34 = -0.001
imageView.layer.transform = transform
imageView.layer.transform = CATransform3DTranslate(imageView.layer.transform, 0, -7.0 * CGFloat(index), 0)
imageView.layer.transform = CATransform3DScale(imageView.layer.transform, 1 - 0.08 * CGFloat(index), 1, 1)
imageView.layer.opacity = 1 - 0.2 * Float(index)
}這個方法用來 創建一個卡片,只用傳入索引(用來初始化)就可以了,它會創建一個 UIImageView,並設置一些所有卡片共有的屬性,然後調用上面的方法進行初始化,最後給卡片添加兩個手勢。
func createOneImageView(index: Int) -> UIImageView {
let imageView = UIImageView()
imageView.contentMode = UIViewContentMode.ScaleAspectFill
imageView.frame = CGRectInset(self.view.frame, 20, 100)
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true
setUpImageView(imageView, index: index)
//點擊手勢
let tap = UITapGestureRecognizer(target: self, action: Selector("tapPanGesture:"))
imageView.addGestureRecognizer(tap)
//滑動手勢
let pan = UIPanGestureRecognizer(target: self, action: Selector("panPanGesture:"))
imageView.addGestureRecognizer(pan)
imageView.userInteractionEnabled = true
return imageView
}第三個是一次性創建多個卡片,傳入數量即可。它會調用循環調用上面的方法創建若干個卡片,並把它們添加到 self.view 上 和 一個全局數組中,以供後面使用。
func createImageViews(count: Int) {
for index in 0..《count { //顯示原因,請將《自行改為英文
let imageView = createOneImageView(index)
imageView.image = UIImage(named: String(format: "Taylor Swift d", arguments: [index % 5]))
self.view.insertSubview(imageView, atIndex: 1)
self.imageViews.append(imageView)
}
}滑動手勢
手勢中,有兩個地方很重要,一個是滑動中,一個是滑動結束。在滑動中需要實時改變每個卡片的位置,還好監測是否超過規定距離,如果超過距離需要移除最上層的卡片,並讓其他卡片復位,再然後讓每層卡片向前移動,最後創建一個新的卡片添加到最後。在滑動結束後需要讓每個卡片復位。
滑動中
如果沒有超過規定距離,就改變每個卡片的位置。通過 view.layer.transform 屬性改變。為了方便,我這裡使用 KVC 來設置形變值。
for index in 0..《self.imageViews.count { //顯示原因,請將《自行改為英文
let imageView = self.imageViews[index]
imageView.layer.setValue((1 - (CGFloat(index) / CGFloat(self.imageViews.count))) * delta, forKeyPath: "transform.translation.x")
imageView.layer.setValue((1 - (CGFloat(index) / CGFloat(self.imageViews.count))) * (delta / self.maxLength) * (15.0 / 180) * CGFloat(M_PI), forKeyPath: "transform.rotation.z")
}如果超過了規定值,就是用動畫讓第一個視圖移出屏幕外。注意:當調用 pan.enabled = false 後,會再次進入手勢監聽方法,並且手勢的狀態為 Cancelled。
pan.enabled = false
let imageView = self.imageViews.first
let current = imageView?.layer.valueForKeyPath("transform.translation.x") as! CGFloat
UIView.animateWithDuration(0.5, animations: { () -> Void in
imageView?.layer.setValue((current > 0) ? self.view.bounds.width : -self.view.bounds.width, forKeyPath: "transform.translation.x")
}, completion: nil)滑動結束
所以在手勢結束(pan.state == .Ended || pan.state == .Cancelled)時需要判斷 pan.enable 屬性,如果 pan.enable == true,說明沒有超過規定值,只用將所有卡片復位就可以了,如果 pan.enable == false,說明超過了規定值,就需要將第一張卡片從父視圖移除,並添加到復用的數組中,然後讓其他的卡片依次前移。
else if pan.state == .Ended || pan.state == .Cancelled {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
for index in 0.. Void in
if !pan.enabled {
pan.enabled = true
let first = self.imageViews.removeAtIndex(0)
first.removeFromSuperview()
self.resueArray.append(first)
self.endAnimation()
}
})
}最後我調用了 self.endAnimation() 方法。這個方法就是將數組中所有卡片向前移動的動畫。
func endAnimation() {
for index in 0.. Void in
imageView.layer.setValue(-7.0 * CGFloat(index), forKeyPath: "transform.translation.y")
imageView.layer.setValue(1 - 0.08 * CGFloat(index), forKeyPath: "transform.scale.x")
imageView.layer.opacity = 1 - 0.2 * Float(index)
}, completion: {(finish: Bool) -> Void in
//最後一個動畫完畢後,添加新的Card到最後
if index == self.imageViews.count - 1 {
self.addNewCard()
}
})
}
}所有卡片移動完成後,調用 `` 方法,將一個新的卡片添加到最後。這個方法中,將判斷重用數組中有沒有卡片,如果沒有,就創建一個,如果有,就直接拿來改變內容就可以了。最後將卡片添加到數組中。
func addNewCard() {
var imageView: UIImageView
if self.resueArray.isEmpty {
imageView = createOneImageView(self.imageViews.count)
} else {
imageView = self.resueArray.removeAtIndex(0)
setUpImageView(imageView, index: self.imageViews.count)
}
imageView.image = UIImage(named: String(format: "Taylor Swift d", arguments: [arc4random_uniform(5)]))
self.view.insertSubview(imageView, atIndex: 1)
self.imageViews.append(imageView)
}到這裡,我的示例中的內容都講完了,獲取完整源代碼請移步: GitHub
但是這和目標中的效果還有一段距離,下面就是一些問題,希望大家能指導一下。
存在的問題
效果感覺不是很流暢,大家可以下載源碼感受一下,估計是移動過程中的代碼有些麻煩,太過復雜。
這裡的重用數組其實就裝了一個卡片,因為移除一個添加一個,所以感覺沒必要這麼重用。誰有好點的想法希望告訴我一下。
最重要的一點,點擊翻轉效果,我在點擊監聽方法中是這麼寫的:
UIView.animateWithDuration(0.5, animations: { () -> Void in
imageView.layer.transform = CATransform3DRotate(imageView.layer.transform, CGFloat(M_PI), 0, 1, 0)
})運行之後卻是下面這個叼樣子!目前還不知道為什麼會這樣,所以誰知道為什麼或者有什麼好的方法實現點擊翻轉效果,請一定要告訴我。

更新
上面第三個問題解決方法:
修改最上面的卡片的 layer.zPosition 屬性,設置的足夠大就可以解決這個問題。可以在點擊的方法裡修改。代碼已更新。感謝 @從今以後 的解決方法!