本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimations。html
到目前爲止,以前的文章只使用了二維動畫——這是在平面設備屏幕上動畫元素的最天然方式。 畢竟,從iOS 7扁平化後的世界中的按鈕,文本字段,開關和圖像沒有了第三維; 這些元素存在於由X和Y軸定義的平面中:ios
核心動畫能夠幫助咱們擺脫這個二維世界; 雖然它不是真正的3D框架,但核心動畫有不少好的方法能夠幫助咱們在3D空間中描繪二維對象。git
換句話說,圖層和動畫仍然以二維方式進行描繪,但能夠在3D空間中旋轉和定位每一個元素的2D平面,以下所示:github
上面顯示的是在3D空間中旋轉的兩個2D圖像。 透視變形使咱們能夠從渲染器的角度瞭解它們的位置。swift
本文將學習如何在3D空間中定位和旋轉圖層。CATransform3D
相似於CGAffineTransform
,但除了在x和y方向上縮放,傾斜和平移以外,它還帶來了第三維:z。 z軸直接從設備屏幕朝向您的眼睛。數組
請考慮如下幾個示例,以更好地瞭解透視的工做原理。緩存
將相機設置得很是靠近屏幕會相應地扭曲圖層的視角:閉包
若是將相機離物體比較遠時的視角:框架
最後,若是你在相機和屏幕之間設置了很大的距離:ide
預覽:
24-簡單的3D動畫 —— 嘗試新發現的有關相機距離和視角的知識。設置圖層的透視圖,處理圖層的變換以旋轉,平移和縮放三維圖層。
25-中級3D動畫 —— 在前一章的基礎上,既然知道了m34和相機距離的祕密,就能夠建立具備多個視圖的各類3D動畫。
本章將嘗新發現的有關相機距離和視角的知識。
開始項目 Office Buddy是一個辦公室幫助應用程序,供員工訪問有關平常公司生活的分類信息。這個應用很簡單就是點擊左上角的按鈕或者左右滑到,而後左邊側欄出現。下面👇將向這個開始項目中添加一些3D元素。
開始項目預覽:
打開ContainerViewController.swift
,ContainerViewController
在屏幕上顯示菜單視圖控制器和內容視圖控制器。 它還處理平移手勢,以便用戶能夠打開和關閉菜單。
您的第一個任務是構建一個類方法,該方法爲側面菜單的給定百分比「開放性」建立相應的3D變換。 將如下方法聲明添加到ContainerViewController
:
func menuTransform(percent: CGFloat) -> CATransform3D {
}
複製代碼
上述方法接受菜單當前進度的單個參數,該參數由handleGesture(_ :)
中的代碼計算,並返回CATransform3D
的實例。 您將直接將此方法的結果分配給菜單圖層的transform屬性。
將如下代碼添加到上面方法中:
var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
複製代碼
這段代碼可能看起來有點使人驚訝; 到目前爲止,您只使用函數來建立或修改變換。 可是,這一次,您正在修改其中一個類的屬性。
注意:
CATransform3D
和CGAffineTransform
分表表示4*4
和3*3
的數學矩陣,在Swift和OC中都是用結構體表示的。屬性
m34
指矩陣的第3行第4列,這個屬性比較經常使用,表示透視效果,m34 = -1 / D
,D能夠理解爲相機距離,D越小,透視效果越明顯,必須在有旋轉效果的前提下,纔會看到透視效果。
對於普通應用程序中的UI元素,相機距離大概能夠表示:
0.1 ... 500:很是接近,透視失真。
750 ... 2,000:視角不錯,內容清晰可見。
2000+:幾乎沒有透視失真。
對於Office Buddy應用程序,1000點的距離將爲菜單提供一個很是微妙的視角。
將如下代碼添加到menuTransform(percent:)
的底部:
let remainingPercent = 1.0 - percent
let angle = remainingPercent * .pi * -0.5
複製代碼
將如下代碼添加到menuTransform(percent:)
的底部:
let rotationTransform = CATransform3DRotate(identity, angle, 0.0, 1.0, 0.0)
let translationTransform = CATransform3DMakeTranslation(menuWidth * percent, 0, 0)
return CATransform3DConcat(rotationTransform, translationTransform)
複製代碼
在這裏,使用rotationTransform
將圖層繞y軸旋轉。 菜單從左側移動,所以還須要建立平移變換以沿x軸移動它,最終將菜單寬度設置爲100%。 最後,鏈接兩個轉換並返回結果。
從setMenu(toPercent:)
中刪除下面:
menuViewController.view.frame.origin.x = menuWidth * CGFloat(percent) - menuWidth
複製代碼
替代爲:
menuViewController.view.layer.transform = menuTransform(percent: percent)
複製代碼
菜單欄的位置經過轉換來控制了。
運行項目, 向右平移查看菜單如何圍繞其y軸旋轉:
菜單以3D形式旋轉,但它圍繞其水平中心旋轉,菜單與內容視圖控制器中間有間隙。
默認狀況下,圖層的錨點的x座標爲0.5,表示它位於中心。 將錨點的x設置爲1.0,就不會出現上面的那種間隙,以下所示:
全部變換都是圍繞圖層的錨點計算的。
在viewDidLoad()
中找到如下行:
menuViewController.view.frame = CGRect(x: -menuWidth, y: 0, width: menuWidth, height: view.frame.height)
複製代碼
如今在該行上方插入如下代碼(在設置視圖幀以前插入行很是重要,不然設置錨點將偏移視圖):
menuViewController.view.layer.anchorPoint.x = 1.0
複製代碼
這會使菜單圍繞其右邊緣旋轉。
運行效果:
這看起來好多了!
陰影爲3D動畫帶來了不少真實感。這裏不須要使用任何先進的着色技術,只要旋轉時更改alpha
。
將如下代碼添加到setMenu(toPercent:)
:
menuViewController.view.alpha = CGFloat(max(0.2, percent))
複製代碼
0.2讓菜單最小還可見,百分比讓菜單越小透明度越低。
因爲此應用程序的背景爲黑色,所以下降菜單視圖的alpha值會使菜單中顯示黑色並模擬陰影效果。
運行效果:
這是一個讓3D效果更加真實的小細節。
若是仔細觀察,會發現第一次點擊按鈕時,菜單不是以3D效果展現,之後纔是。這是由於第一次切換菜單以前,設置3D動畫參數和圖層轉換。在viewDidLoad()
中添加:
setMenu(toPercent: 0.0)
複製代碼
讓動畫更加「完美」。若是在來回平移時盯着菜單足夠長,會注意到菜單項的邊框看起來像素化,以下所示:
核心動畫不斷重繪菜單視圖控制器的全部內容,並在全部元素移動時從新計算全部元素的透視失真,這個過程當中會出現鋸齒狀邊緣。
最好讓Core Animation知道咱們不會在動畫期間更改菜單內容,以便它能夠渲染菜單一次並簡單地旋轉渲染和緩存的圖像。 這聽起來很複雜,但很容易實現。
找到handleGesture()
中的.began
代碼塊,此代碼在用戶平移操做時執行。
將如下代碼添加到.began
代碼塊的末尾:
menuViewController.view.layer.shouldRasterize = true
menuViewController.view.layer.rasterizationScale = UIScreen.main.scale
複製代碼
shouldRasterize
讓核心動畫將圖層內容緩存爲圖像。 而後設置rasterizationScale
以匹配當前的屏幕比例。
運行,效果:
爲避免在使用應用程序時進行任何沒必要要的緩存,應該在動畫完成後當即關閉光柵化。 在.failed
代碼塊找到動畫完成閉包並添加如下代碼:
self.menuViewController.view.layer.shouldRasterize = false
複製代碼
如今,只在動畫期間激活光柵化。提升了效率!😊
菜單展現時,菜單按鈕也進行自身的旋轉。具體來講,您將圍繞x軸和y軸建立旋轉,以使菜單按鈕在其對角線上翻轉。
在ContainerViewController
的setMenu(toPercent:)
中添加:
let centerVC = centerViewController.viewControllers.first as? CenterViewController
if let menuButton = centerVC?.menuButton {
menuButton.imageView.layer.transform = buttonTransform(percent: percent)
}
複製代碼
buttonTransform
函數爲:
func buttonTransform(percent: CGFloat) -> CATransform3D {
var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
let angle = percent * .pi
let rotationTransform = CATransform3DRotate(identity, angle, 1.0, 1.0, 0.0)
return rotationTransform
}
複製代碼
效果以下:
在上一章24-簡單3D動畫中,學習了將透視應用到單個視圖製做出簡單的3D效果的動畫; 事實上,一旦咱們知道m34和相機距離的祕密,就能夠建立各類3D動畫。
本章之前面的內容爲基礎,學習如何使用多個視圖建立有意思的3D動畫。
本章的開始項目 ***ImageGallery***是一個簡單的颶風圖庫。
本章的開始項目是:
只是一個空白屏幕,頂部有兩個按鈕。
打開ViewController.swift
,會看到一個名爲images
的數組,此數組就是一些圖片信息。
ImagViewCard
類繼承自UIImageView
而且有一個字符串屬性title
來保存颶風標題,有一個名爲didSelect
的屬性,以便您能夠輕鬆地在圖像上設置點擊處理程序。
第一個任務是將全部圖像添加到視圖控制器的視圖中。 將如下代碼添加到viewDidAppeae(_:)
的末尾:
for image in images {
image.layer.anchorPoint.y = 0.0
image.frame = view.bounds
view.addSubview(image)
}
複製代碼
在上面的代碼中,循環遍歷全部圖像,在y軸上將每一個圖像的錨點設置爲0.0,並調整每一個圖像的大小,使其佔據整個屏幕。 設置錨點可以讓圖像圍繞其上邊緣而不是中心的默認值旋轉,以下圖所示:
運行只會看到最後一張圖片Hurricane Irene
,由於圖片位置相同,疊加在一塊兒來
顯示颶風圖像的名字,在viewDidAppear(_:)
的末尾添加如下行:
navigationItem.title = images.last?.title
複製代碼
注意,目前沒有在圖像上設置任何透視轉換;以後將直接在視圖控制器的視圖上設置透視圖。
在上一章中,在單個視圖上調整了transfor
m屬性,而後在3D空間中旋轉它。可是,因爲您當前的項目有更多的我的視圖,須要在3D中操做,您能夠設置其父視圖的透視圖,從而節省大量工做。
將如下代碼添加到viewDidAppear(_:)
:
var perspective = CATransform3DIdentity
perspective.m34 = -1.0/250.0
view.layer.sublayerTransform = perspective
複製代碼
在這裏,您可使用圖層屬性sublayerTransform
來設置視圖控制器圖層的全部子圖層的透視圖。 而後將子層轉換與每一個單獨層的自身變換組合。
這使您能夠專一於管理子視圖的旋轉或平移,而無需擔憂透視。 您將在下一節中更詳細地瞭解它的工做原理。
toggleGallery(_:)
鏈接着右上方的「瀏覽」按鈕,在此處將3D變換應用於四個圖像。
將如下變量添加到toggleGallery(_:)
:
var imageYOffset: CGFloat = 50.0
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
}
複製代碼
因爲您不僅是將全部圖像旋轉到原位而只是移動它們以產生」扇形「動畫,所以您可使用imageYOffset
來設置每一個圖像的偏移。 接下來,您須要遍歷全部圖像並運行其各自的動畫。
在這裏,您循環瀏覽視圖控制器視圖的全部子視圖,並僅對做爲ImageViewCard
實例的子視圖執行操做。 在上面添加的guard
塊以後添加如下代碼,以替換此處的更多代碼註釋:
var imageTransform = CATransform3DIdentity
// 1
imageTransform = CATransform3DTranslate(imageTransform, 0.0, imageYOffset, 0.0)
// 2
imageTransform = CATransform3DScale(imageTransform, 0.95, 0.6, 1.0)
// 3
imageTransform = CATransform3DRotate(imageTransform, .pi/8, -1.0, 0.0, 0.0)
複製代碼
首先將標識轉換分配給imageTransform,而後對其添加一系列調整。 這是每一個單獨的調整對圖像的做用:
// 1
使用CATransform3DTranslate
在y軸上移動圖像; 這會使圖像偏離其默認的0.0 y座標,以下所示:
以後,將要分別計算每一個圖像的imageYOffset
,不然圖片仍是疊加在一塊兒。
// 2
經過使用CATransform3DScale
調整轉換的比例份量來縮放圖像。 能夠在x軸上稍微縮小圖像,可是在y軸上將其縮小到60%以豐富旋轉3D效果:
// 3
最後,使用CATransform3DRotate
將圖像旋轉22.5度,使其具備一些透視變形,以下所示:
請記住,以前已經設置了錨點,所以圖像圍繞其頂部邊緣旋轉。
如今你看到經過view.layer.sublayerTransform設置上面的m34值的值; 您的旋轉變換隻需從新使用子層變換中的m34值,而無需在此處應用它。 那很方便!
如今剩下的就是將轉換應用於每一個圖像。 添加如下行(仍在for代碼塊中):
image.layer.transform = imageTransform
複製代碼
將如下行添加到for塊的末尾,修改每一個圖像的位置:
imageYOffset += view.frame.height / CGFloat(images.count)
複製代碼
這會調整每一個圖像的y偏移量,具體取決於它在堆棧中的位置。 將屏幕高度除以圖像數量,以便它們在屏幕上均勻分佈。 運行後效果:
下面讓它動起來!
在上面的image.layer.transform = imageTransform
的前面添加:
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: image.layer.transform)
animation.toValue = NSValue(caTransform3D: imageTransform)
animation.duration = 0.33
image.layer.add(animation, forKey: nil)
複製代碼
這段代碼很是熟悉:在transform屬性上建立一個圖層動畫,並將其從當前值設置爲以前設計的imageTransform
。 運行後, 點擊「瀏覽」按鈕,效果:
你如今已經完成了畫廊; 當您在用戶點擊「瀏覽」按鈕時添加關閉風扇的功能時,您將在「挑戰」部分從新訪問它。
爲圖像庫添加一點交互性:點擊圖像,變成全屏,而且位置移到最前面,以便用戶能夠更好地查看它。
ImageViewCard
已經具備名爲didSelect
的閉包表達式屬性,當用戶點擊圖像,就將點擊的圖像視圖做爲輸入參數給這個閉包。
首先將如下代碼添加viewDidAppear()
的for循環體內:
image.didSelect = selectImage
複製代碼
在ViewController
中添加方法:
func selectImage(selectedImage: ImageViewCard) {
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
if image === selectedImage {
} else {
}
}
}
複製代碼
如今您還須要兩個動畫:一個用於爲所選圖像設置動畫,另外一個用於爲圖庫中的全部其餘圖像設置動畫。 你將反過來解決這個問題並首先淡出未選擇的圖像。
上面的方法還缺乏兩個動畫,當image === selectedImage
,就是所選圖像的動畫;或者,未選擇的全部其餘圖像的動畫,前者代碼爲:
UIView.animate(withDuration: 0.33, delay: 0.0, options: .curveEaseIn, animations: {
image.alpha = 0.0
}, completion: { (_) in
image.alpha = 1.0
image.layer.transform = CATransform3DIdentity
})
複製代碼
後者代碼爲:
UIView.animate(withDuration: 0.33, delay: 0.0, options: .curveEaseIn, animations: {
image.layer.transform = CATransform3DIdentity
}, completion: {_ in
self.view.bringSubview(toFront: image)
})
複製代碼
在這裏,沒有對動畫進行3D變換,而後確保圖像位於視圖堆棧的頂部,以便它可見。
最後,將如下代碼添加到selectImage(selectedImage:)
的末尾,更新標題:
self.navigationItem.title = selectedImage.title
複製代碼
這小結工做是將使「瀏覽」按鈕能夠關閉圖庫視圖。
向ViewController
添加一個isGalleryOpen
的新屬性,並將其初始值設置爲false
。
須要在代碼中的兩個位置更新此屬性的值:
toggleGallery(_:)
結束時將其設置爲true
selectImage(selectedImage:)
結束時將其設置爲false
在toggleGallery()
的頂部,添加一個檢查以查看圖庫是否已打開。 若是打開,則遍歷全部圖像並將其轉換設置爲原始值。 不要忘記重置isGalleryOpen
並返回,所以其他的方法代碼也不會執行。
if isGalleryOpen {
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: image.layer.transform)
animation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.duration = 0.33
image.layer.add(animation, forKey: nil)
image.layer.transform = CATransform3DIdentity
}
isGalleryOpen = false
return
}
複製代碼
本章的最後效果:
本文在個人我的博客中地址:系統學習iOS動畫之六:3D動畫