本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細程式碼都放在我的Github上 andyRon/LearniOSAnimations。
到目前為止,之前的文章只使用了二維動畫——這是在平面裝置螢幕上動畫元素的最自然方式。 畢竟,從iOS 7扁平化後的世界中的按鈕,文字欄位,開關和影像沒有了第三維; 這些元素存在於由X和Y軸定義的平面中:
核心動畫可以幫助我們擺脫這個二維世界; 雖然它不是真正的3D框架,但核心動畫有很多好的方法可以幫助我們在3D空間中描繪二維物件。
換句話說,圖層和動畫仍然以二維方式進行描繪,但可以在3D空間中旋轉和定位每個元素的2D平面,如下所示:
上面顯示的是在3D空間中旋轉的兩個2D影像。 透視變形使我們可以從渲染器的角度瞭解它們的位置。
本文將學習如何在3D空間中定位和旋轉圖層。CATransform3D
類似於CGAffineTransform
,但除了在x和y方向上縮放,傾斜和平移之外,它還帶來了第三維:z。 z軸直接從裝置螢幕朝向您的眼睛。
請考慮以下幾個示例,以更好地瞭解透視的工作原理。
將相機設定得非常靠近螢幕會相應地扭曲圖層的視角:
如果將相機離物體比較遠時的視角:
最後,如果你在相機和螢幕之間設定了很大的距離:
預覽:
24-簡單的3D動畫 —— 嘗試新發現的有關相機距離和視角的知識。設定圖層的透檢視,處理圖層的變換以旋轉,平移和縮放三維圖層。
25-中級3D動畫 —— 在前一章的基礎上,既然知道了m34和相機距離的祕密,就可以建立具有多個檢視的各種3D動畫。
24-簡單3D動畫
本章將嘗新發現的有關相機距離和視角的知識。
開始專案 Office Buddy是一個辦公室幫助應用程式,供員工訪問有關日常公司生活的分類資訊。這個應用很簡單就是點選左上角的按鈕或者左右滑到,然後左邊側欄出現。下面?將向這個開始專案中新增一些3D元素。
開始專案預覽:
創造3Dtransformations
開啟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
複製程式碼
現在,只在動畫期間啟用光柵化。提高了效率!?
選單按鈕的3D旋轉動畫
選單展示時,選單按鈕也進行自身的旋轉。具體來說,您將圍繞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
}
複製程式碼
效果如下:
25-中級3D動畫
在上一章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動畫