系統學習iOS動畫之六:3D動畫

Andy_Ron發表於2018-12-27

本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細程式碼都放在我的Github上 andyRon/LearniOSAnimations

到目前為止,之前的文章只使用了二維動畫——這是在平面裝置螢幕上動畫元素的最自然方式。 畢竟,從iOS 7扁平化後的世界中的按鈕,文字欄位,開關和影像沒有了第三維; 這些元素存在於由X和Y軸定義的平面中:

系統學習iOS動畫之六:3D動畫

核心動畫可以幫助我們擺脫這個二維世界; 雖然它不是真正的3D框架,但核心動畫有很多好的方法可以幫助我們在3D空間中描繪二維物件。

換句話說,圖層和動畫仍然以二維方式進行描繪,但可以在3D空間中旋轉和定位每個元素的2D平面,如下所示:

系統學習iOS動畫之六:3D動畫

上面顯示的是在3D空間中旋轉的兩個2D影像。 透視變形使我們可以從渲染器的角度瞭解它們的位置。

本文將學習如何在3D空間中定位和旋轉圖層。CATransform3D類似於CGAffineTransform,但除了在x和y方向上縮放,傾斜和平移之外,它還帶來了第三維:z。 z軸直接從裝置螢幕朝向您的眼睛。

請考慮以下幾個示例,以更好地瞭解透視的工作原理。

將相機設定得非常靠近螢幕會相應地扭曲圖層的視角:

image-20181204230708868

如果將相機離物體比較遠時的視角:

image-20181204230723724

最後,如果你在相機和螢幕之間設定了很大的距離:

image-20181204230830029

預覽:

24-簡單的3D動畫 —— 嘗試新發現的有關相機距離和視角的知識。設定圖層的透檢視,處理圖層的變換以旋轉,平移和縮放三維圖層。

25-中級3D動畫 —— 在前一章的基礎上,既然知道了m34和相機距離的祕密,就可以建立具有多個檢視的各種3D動畫。

24-簡單3D動畫

本章將嘗新發現的有關相機距離和視角的知識。

開始專案 Office Buddy是一個辦公室幫助應用程式,供員工訪問有關日常公司生活的分類資訊。這個應用很簡單就是點選左上角的按鈕或者左右滑到,然後左邊側欄出現。下面?將向這個開始專案中新增一些3D元素。

開始專案預覽:

系統學習iOS動畫之六:3D動畫

創造3Dtransformations

開啟ContainerViewController.swiftContainerViewController在螢幕上顯示選單檢視控制器和內容檢視控制器。 它還處理平移手勢,以便使用者可以開啟和關閉選單。

您的第一個任務是構建一個類方法,該方法為側面選單的給定百分比“開放性”建立相應的3D變換。 將以下方法宣告新增到ContainerViewController

func menuTransform(percent: CGFloat) -> CATransform3D {

}
複製程式碼

上述方法接受選單當前進度的單個引數,該引數由handleGesture(_ :)中的程式碼計算,並返回CATransform3D的例項。 您將直接將此方法的結果分配給選單圖層的transform屬性。

將以下程式碼新增到上面方法中:

var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
複製程式碼

這段程式碼可能看起來有點令人驚訝; 到目前為止,您只使用函式來建立或修改變換。 但是,這一次,您正在修改其中一個類的屬性。

注意:CATransform3DCGAffineTransform分表表示4*43*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軸旋轉:

系統學習iOS動畫之六:3D動畫

選單以3D形式旋轉,但它圍繞其水平中心旋轉,選單與內容檢視控制器中間有間隙。

移動圖層的錨點

預設情況下,圖層的錨點的x座標為0.5,表示它位於中心。 將錨點的x設定為1.0,就不會出現上面的那種間隙,如下所示:

image-20181205104831488

所有變換都是圍繞圖層的錨點計算的。

viewDidLoad()中找到以下行:

menuViewController.view.frame = CGRect(x: -menuWidth, y: 0, width: menuWidth, height: view.frame.height)
複製程式碼

現在在該行上方插入以下程式碼(在設定檢視幀之前插入行非常重要,否則設定錨點將偏移檢視):

menuViewController.view.layer.anchorPoint.x = 1.0
複製程式碼

這會使選單圍繞其右邊緣旋轉。

執行效果:

系統學習iOS動畫之六:3D動畫

這看起來好多了!

通過陰影建立遠景

陰影為3D動畫帶來了很多真實感。這裡不需要使用任何先進的著色技術,只要旋轉時更改alpha

將以下程式碼新增到setMenu(toPercent:)

menuViewController.view.alpha = CGFloat(max(0.2, percent))
複製程式碼

0.2讓選單最小還可見,百分比讓選單越小透明度越低。

由於此應用程式的背景為黑色,因此降低選單檢視的alpha值會使選單中顯示黑色並模擬陰影效果。

執行效果:

系統學習iOS動畫之六:3D動畫

這是一個讓3D效果更加真實的小細節。

如果仔細觀察,會發現第一次點選按鈕時,選單不是以3D效果展示,以後才是。這是因為第一次切換選單之前,設定3D動畫引數和圖層轉換。在viewDidLoad()中新增:

setMenu(toPercent: 0.0)
複製程式碼

光柵化的效率

讓動畫更加“完美”。如果在來回平移時盯著選單足夠長,會注意到選單項的邊框看起來畫素化,如下所示:

image-20181205110350367

核心動畫不斷重繪選單檢視控制器的所有內容,並在所有元素移動時重新計算所有元素的透視失真,這個過程中會出現鋸齒狀邊緣

最好讓Core Animation知道我們不會在動畫期間更改選單內容,以便它可以渲染選單一次並簡單地旋轉渲染和快取的影像。 這聽起來很複雜,但很容易實現。

找到handleGesture()中的.began程式碼塊,此程式碼在使用者平移操作時執行。

將以下程式碼新增到.began程式碼塊的末尾:

menuViewController.view.layer.shouldRasterize = true
menuViewController.view.layer.rasterizationScale = UIScreen.main.scale
複製程式碼

shouldRasterize讓核心動畫將圖層內容快取為影像。 然後設定rasterizationScale以匹配當前的螢幕比例。

執行,效果:

image-20181205110814153

為避免在使用應用程式時進行任何不必要的快取,應該在動畫完成後立即關閉光柵化。 在.failed程式碼塊找到動畫完成閉包並新增以下程式碼:

self.menuViewController.view.layer.shouldRasterize = false
複製程式碼

現在,只在動畫期間啟用光柵化。提高了效率!?

選單按鈕的3D旋轉動畫

選單展示時,選單按鈕也進行自身的旋轉。具體來說,您將圍繞x軸和y軸建立旋轉,以使選單按鈕在其對角線上翻轉。

ContainerViewControllersetMenu(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
}
複製程式碼

效果如下:

系統學習iOS動畫之六:3D動畫

25-中級3D動畫

在上一章24-簡單3D動畫中,學習了將透視應用到單個檢視製作出簡單的3D效果的動畫; 事實上,一旦我們知道m34和相機距離的祕密,就可以建立各種3D動畫。

本章以前面的內容為基礎,學習如何使用多個檢視建立有意思的3D動畫。

本章的開始專案 ***ImageGallery***是一個簡單的颶風相簿。

探索開始專案

本章的開始專案是:

image-20181212190316969

只是一個空白螢幕,頂部有兩個按鈕。

開啟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,並調整每個影像的大小,使其佔據整個螢幕。 設定錨點可讓影像圍繞其上邊緣而不是中心的預設值旋轉,如下圖所示:

image-20181205113638430

執行只會看到最後一張圖片Hurricane Irene,因為圖片位置相同,疊加在一起來

顯示颶風影像的名字,在viewDidAppear(_:)的末尾新增以下行:

navigationItem.title = images.last?.title
複製程式碼

注意,目前沒有在影像上設定任何透視轉換;之後將直接在檢視控制器的檢視上設定透檢視。

在上一章中,在單個檢視上調整了transform屬性,然後在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座標,如下所示:

image-20181212182739840

之後,將要分別計算每個影像的imageYOffset,否則圖片還是疊加在一起。

// 2 通過使用CATransform3DScale調整轉換的比例分量來縮放影像。 可以在x軸上稍微縮小影像,但是在y軸上將其縮小到60%以豐富旋轉3D效果:

image-20181212182903109

// 3 最後,使用CATransform3DRotate將影像旋轉22.5度,使其具有一些透視變形,如下所示:

image-20181212182944370

請記住,之前已經設定了錨點,因此影像圍繞其頂部邊緣旋轉。

現在你看到通過view.layer.sublayerTransform設定上面的m34值的值; 您的旋轉變換隻需重新使用子層變換中的m34值,而無需在此處應用它。 那很方便!

現在剩下的就是將轉換應用於每個影像。 新增以下行(仍在for程式碼塊中):

image.layer.transform = imageTransform
複製程式碼

將以下行新增到for塊的末尾,修改每個影像的位置:

imageYOffset += view.frame.height / CGFloat(images.count)
複製程式碼

這會調整每個影像的y偏移量,具體取決於它在堆疊中的位置。 將螢幕高度除以影像數量,以便它們在螢幕上均勻分佈。 執行後效果:

image-20181205115546758

下面讓它動起來!

動畫相簿

在上面的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。 執行後, 點選“瀏覽”按鈕,效果:

系統學習iOS動畫之六:3D動畫

你現在已經完成了畫廊; 當您在使用者點選“瀏覽”按鈕時新增關閉風扇的功能時,您將在“挑戰”部分重新訪問它。

更多一點互動

為影像庫新增一點互動性:點選影像,變成全屏,並且位置移到最前面,以便使用者可以更好地檢視它。

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動畫

本文在我的個人部落格中地址:系統學習iOS動畫之六:3D動畫

相關文章