系統學習iOS動畫之一:檢視動畫

Andy_Ron發表於2018-12-27

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

這個部分介紹UIKit動畫API,這些API專門用於輕鬆製作檢視動畫(View Animations),同時避免核心動畫(Core Animation)(見系統學習iOS動畫之三:圖層動畫)的複雜性。

UIKit動畫API不僅易於使用,而且提供了大量靈活性和強大功能,可以處理大多數(當然不是全部)動畫要求。

UIKit動畫API可以在螢幕上為最終繼承自UIView的任何物件設定動畫,例如:UILabelUIImageViewUIButton等等,也可以是自己建立的任何自定義最終繼承自UIView類。

本文包括五個章節,完成兩個專案BahamaAirLoginScreenFlight Info

BahamaAirLoginScreen 是一個登入頁面專案,1、2、3章節為這個專案的一些UI新增各種動畫。

1-檢視動畫入門 —— 學習如何移動,縮放和淡化檢視等基本的UIKit API。
2-彈簧動畫 —— 線上性動畫的概念基礎上,使用彈簧動畫創造出更引人注目的效果。?
3-過渡動畫 —— 檢視的出現和消失。

Flight Info 是一個航班狀態變化專案,4、5章節用一些高階一點動畫來完成這個專案。

4-練習檢視動畫 —— 練習前面學到的動畫技術。
5-關鍵幀動畫 —— 使用關鍵幀動畫來建立由許多不同階段組成的複雜動畫。

1-檢視動畫入門

第一個動畫

開始專案 BahamaAirLoginScreen是一個簡單的登入頁面,有兩個TextField,一個Label,一個Button,4個雲圖片和一個背景圖片,效果如下:

系統學習iOS動畫之一:檢視動畫

讓Label和兩個TextField在檢視顯示之前移動到螢幕外。在viewWillAppear()中新增:

heading.center.x    -=  view.bounds.width
username.center.x   -=  view.bounds.width
password.center.x   -=  view.bounds.width
複製程式碼

系統學習iOS動畫之一:檢視動畫

新增Label和兩個TextField進入螢幕的動畫,在viewDidAppear()中新增:

UIView.animate(withDuration: 0.5) {
  self.heading.center.x += self.view.bounds.width
}

UIView.animate(withDuration: 0.5, delay: 0.3, options: [],
  animations: {
    self.username.center.x += self.view.bounds.width
  }, 
  completion: nil
)

UIView.animate(withDuration: 0.5, delay: 0.4, options: [],
  animations: {
    self.password.center.x += self.view.bounds.width
  }, 
  completion: nil
)
複製程式碼

這樣heading和TextField就有了前後分別進入螢幕的動畫。

類似UIView.animate(...)的方法,根據引數的不同有好幾個,不同引數的意義:

withDuration :動畫持續時間。

delay :動畫開始之前的延遲時間。

optionsUIView.AnimationOptions的陣列,用來定義動畫的變化形式,之後會詳細說明。

animations :提供動畫的閉包,也就是動畫程式碼。

completion :動畫執行完成後的閉包 。

還有 usingSpringWithDampinginitialSpringVelocity之後章節會提到。

可動畫屬性

前面,使用center建立簡單的位置變化檢視動畫。

並非所有檢視屬性都可以設定動畫,但所有檢視動畫(從最簡單到最複雜)都可以通過動畫檢視上的屬性來構建。下面來看看可用於動畫的屬性有哪些:

位置的大小

bounds frame center

系統學習iOS動畫之一:檢視動畫

外形(Appearance)

backgroundColor
alpha : 可建立淡入和淡出效果。

系統學習iOS動畫之一:檢視動畫

轉換(Transformation)

transform : 設定檢視的旋轉,縮放和/或位置的動畫。

image-20181116174323974

這些看起來像是非常基本的動畫,可以製作令人驚訝的複雜動畫效果!?

動畫選項

動畫選項(Animation options)就是之前提到的options引數,它是UIView.AnimationOptions的陣列。UIView.AnimationOptions是結構體,有很多常量值,具體可檢視官方文件

下面說明幾個常用的

重複

.repeat :動畫一直重複。

.autoreverse :如果僅有.repeat引數動畫的過程,就像是 b->e b->e ...,而有了.autoreverse,動畫過程就像是b->e->b->e ...。看下圖很容易看出區別。

系統學習iOS動畫之一:檢視動畫

動畫緩動

Animation easing,我暫且把它叫做 動畫緩動

curve:彎曲;使彎曲。ease:減輕,緩和。

在現實生活中,事物並不只是突然開始或停止移動。 像汽車或火車這樣的物體會慢慢加速直到達到目標速度,除非它們碰到磚牆,否則它們會逐漸減速直到它們完全停在最終目的地。

為了使動畫看起來更逼真,可以在開始時慢慢加速,在結束前放慢速度,一般稱為緩入(ease-in)緩出(ease-out)

.curveLinear :不對動畫應用加速或減速。 .curveEaseIn :動畫的開始時慢,結束時快。

UIView.animate(withDuration: 1, delay: 0.6, options: [.repeat, .autoreverse, .curveEaseIn], animations: {
  self.password.center.x += self.view.bounds.width
}, completion: nil)
複製程式碼

.curveEaseOut :動畫開始時快,結束時慢。

UIView.animate(withDuration: 1, delay: 0.6, options: [.repeat, .autoreverse, .curveEaseOut], animations: {
          self.password.center.x += self.view.bounds.width
      }, completion: nil)
複製程式碼

系統學習iOS動畫之一:檢視動畫

.curveEaseInOut :動畫開始結束都慢,中間快

雲的淡入動畫

這個很好理解,就是雲的UIImageView的透明度變化動畫。先在viewWillAppear()中把雲設定成透明:

cloud1.alpha = 0.0
cloud2.alpha = 0.0
cloud3.alpha = 0.0
cloud4.alpha = 0.0
複製程式碼

然後在viewDidAppear()中新增動畫。

UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
    self.cloud1.alpha = 1.0
}, completion: nil)
UIView.animate(withDuration: 0.5, delay: 0.7, options: [], animations: {
    self.cloud2.alpha = 1.0
}, completion: nil)
UIView.animate(withDuration: 0.5, delay: 0.9, options: [], animations: {
    self.cloud3.alpha = 1.0
}, completion: nil)
UIView.animate(withDuration: 0.5, delay: 1.1, options: [], animations: {
    self.cloud4.alpha = 1.0
}, completion: nil)
複製程式碼

2-彈簧動畫

1-檢視動畫入門中動畫是單一方向上的動作,可以理解為一點移動到另一個。

這一章節是稍微複雜一點的彈簧動畫(Springs)

系統學習iOS動畫之一:檢視動畫

用點變化描述彈簧動畫:

系統學習iOS動畫之一:檢視動畫

檢視從A點到B點,在B點來回遞減振盪,直到檢視在B點停止。這是一個很好的效果, 讓我們的動畫新增了一種活潑,真實的感覺。

本章的開始專案 BahamaAirLoginScreen是上一章節的完成專案。

viewWillAppear()中新增:

loginButton.center.y += 30.0
loginButton.alpha = 0.0
複製程式碼

然後再在viewDidAppear()中新增:

UIView.animate(withDuration: 0.5, delay: 0.5, 
usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {
  self.loginButton.center.y -= 30.0
  self.loginButton.alpha = 1.0
}, completion: nil)
複製程式碼

這樣Log In按鈕就有個向上移動的動畫變成了兩個屬性同時變化的動畫。

usingSpringWithDamping :阻尼引數, 介於0.0 ~ 1.0,接近0.0的值建立一個更有彈性的動畫,而接近1.0的值建立一個看起來很僵硬的效果。 您可以將此值視為彈簧的**“剛度”**。

initialSpringVelocity : 初始速度, 要平滑開始動畫,請將此值與檢視之前的檢視速度相匹配。

效果:

系統學習iOS動畫之一:檢視動畫

與使用者互動的動畫

讓登入按鈕產生一個與使用者互動的動畫,在Log In按鈕的Action logIn()方法中新增:

UIView.animate(withDuration: 1.5, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: [], animations: {
  self.loginButton.bounds.size.width += 80.0
}, completion: nil)
複製程式碼

點選後有個寬度變大的簡單動畫。

繼續在logIn()中新增:

UIView.animate(withDuration: 0.33, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: {
  self.loginButton.center.y += 60.0
}, completion: nil) 
複製程式碼

點選後寬度變大的同時向下移動移動位置。

給使用者反饋的另一個好方法是通過顏色變化。 在上面動畫閉包中新增:

self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
複製程式碼

最後一個給使用者反饋的方法:activity indicator(活動指示器,俗稱菊花轉?)。 登入按鈕應該通過網路啟動使用者身份驗證活動,通過菊花轉讓使用者知道登入操作正在進行。

繼續在上面動畫閉包中新增(spinner已經在viewDidLoad中初始化了,並且alpha設定為0.0):

self.spinner.center = CGPoint(x: 40.0, y: self.loginButton.frame.size.height/2)
self.spinner.alpha = 1.0
複製程式碼

讓菊花轉也隨著登入按鈕的向下移動而移動,最終登入按鈕的效果:

系統學習iOS動畫之一:檢視動畫

把文字框的動畫修改為彈簧動畫

把之前viewDidAppear()中的

UIView.animate(withDuration: 0.5, delay: 0.3, options: [], animations: {
    self.username.center.x += self.view.bounds.width
}, completion: nil)
UIView.animate(withDuration: 1, delay: 0.6, options: [], animations: {
    self.password.center.x += self.view.bounds.width
}, completion: nil)
複製程式碼

修改為:

UIView.animate(withDuration: 0.5, delay: 0.3, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
    self.username.center.x += self.view.bounds.width
}, completion: nil)

UIView.animate(withDuration: 0.5, delay: 0.4, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
    self.password.center.x += self.view.bounds.width
}, completion: nil)
複製程式碼

效果為:

系統學習iOS動畫之一:檢視動畫

3-過渡動畫

過渡動畫(Transitions)

本章節的開始專案 是前一章節的完成專案。

過渡的例子

使用過渡動畫的各種動畫場景。

新增檢視

要在螢幕上新增新檢視的動畫,可以呼叫類似於前面章節中使用的方法。 這次的不同之處在於,需要預先選擇一個預定義的過渡效果,併為動畫容器檢視設定動畫。 過渡動畫是設定在容器檢視上,因此動畫作用在新增到容器檢視的所有子檢視。

下面做一個測試(結束後,刪除相應程式碼繼續之後內容):

var animationContainerView: UIView!

override func viewDidLoad() {
  super.viewDidLoad()
  //設定動畫容器檢視
  animationContainerView = UIView(frame: view.bounds)
  animationContainerView.frame = view.bounds
  view.addSubview(animationContainerView)
}

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  //建立新檢視
  let newView = UIImageView(image: UIImage(named: "banner"))
  newView.center = animationContainerView.center

  //通過過渡動畫增加新檢視
  UIView.transition(with: animationContainerView, 
    duration: 0.33, 
    options: [.curveEaseOut, .transitionFlipFromBottom], 
    animations: {  
      self.animationContainerView.addSubview(newView)
    }, 
    completion: nil
  )
}
複製程式碼

效果:

系統學習iOS動畫之一:檢視動畫

transitionFlipFromBottomtransitionFlipFromLeft替代後的效果:

系統學習iOS動畫之一:檢視動畫

完整的預定義過渡動畫的選項如下,這些動畫選項和上兩節中出現options一樣屬於UIView.AnimationOptions

.transitionFlipFromLeft
.transitionFlipFromRight
.transitionCurlUp
.transitionCurlDown
.transitionCrossDissolve
.transitionFlipFromTop
.transitionFlipFromBottom
複製程式碼

刪除檢視

從螢幕中刪除子檢視的過渡動畫操作和新增類似。

系統學習iOS動畫之一:檢視動畫

參考程式碼:

UIView.transition(with: animationContainerView, duration: 0.33,
                  options: [.curveEaseOut, .transitionFlipFromBottom],
                  animations: {
                      self.newView.removeFromSuperview()
                  },
                  completion: nil
)
複製程式碼

隱藏或顯示檢視

系統學習iOS動畫之一:檢視動畫

新增和刪除都會改變檢視層次結構,這也是需要一個容器檢視的原因。隱藏或顯示的過渡動畫使用檢視本身作為動畫容器。

參考程式碼:

UIView.transition(with: self.newView, duration: 0.33, 
  options: [.curveEaseOut, .transitionFlipFromBottom], 
  animations: {
    self.newView.isHidden = true
  }, 
  completion: nil
)
複製程式碼

一個檢視替代另個檢視

系統學習iOS動畫之一:檢視動畫

參考程式碼:


UIView.transition(from: oldView, to: newView, duration: 0.33, 
  options: .transitionFlipFromTop, completion: nil)
複製程式碼

組合過渡動畫

這一部分將模擬一些使用者身份驗證過程,幾個不同的進度訊息變化的動畫。 一旦使用者點選登入按鈕,將向他們顯示訊息,包括“Connecting...”,“Authorizing”和“Failed”。

ViewController中新增方法showMessage()

func showMessage(index: Int) {
    label.text = messages[index]

    UIView.transition(with: status, duration: 0.33, options: [.curveEaseOut, .transitionCurlDown], animations: {
        self.status.isHidden = false
    }, completion: { _ in

    })  
}
複製程式碼

並在登入按鈕的ActionlogIn方法的下移動畫的completion閉包中新增呼叫self.showMessage(index: 0)

UIView.animate(withDuration: 1.5, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options:[], animations: {
    self.loginButton.bounds.size.width += 80.0
}, completion: { _ in
    self.showMessage(index: 0)
})
複製程式碼

動畫選項.transitionCurlDown的效果,就像一張紙翻下來,看起來如下:

系統學習iOS動畫之一:檢視動畫

這種效果很好的讓靜態文字標籤的訊息得到使用者的關注。

注意:iPhone模擬器提供了慢動畫檢視,方便看清那些比較快動畫的過程,Debug/Slow Animations(Command + T)。

新增一個狀態資訊消除動畫方法:

func removeMessage(index: Int) {
    UIView.animate(withDuration: 0.33, delay: 0.0, options: [], animations: {
        self.status.center.x += self.view.frame.size.width
    }) { (_) in
        self.status.isHidden = true
        self.status.center = self.statusPosition

        self.showMessage(index: index+1) 
     }
}
複製程式碼

這個資訊消除方法在什麼地方呼叫呢?當然是狀態資訊顯示結束後呼叫,因此在showMessage方法的completion閉包中新增:

delay(2.0) {
  if index < self.messages.count-1 {
    self.removeMessage(index: index)
  } else {
    //reset form
  }
}
複製程式碼

恢復初始狀態

當“Connecting...”、“Authorizing”和“Failed”等幾個資訊顯示完後,需要將資訊標籤刪除和將登入按鈕恢復原樣。

新增resetForm()函式:

func resetForm() {
    // 狀態資訊標籤消失動畫
    UIView.transition(with: status, duration: 0.2, options: .transitionFlipFromTop, animations: {
        self.status.isHidden = true
        self.status.center = self.statusPosition
    }, completion: nil)
    // 登入按鈕和菊花轉恢復原來狀態的動畫
    UIView.animate(withDuration: 0.2, delay: 0.0, options: [], animations: {
        self.spinner.center = CGPoint(x: -20.0, y: 16.0)
        self.spinner.alpha = 0.0
        self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
        self.loginButton.bounds.size.width -= 80.0
        self.loginButton.center.y -= 60.0
    }, completion: nil)
}
複製程式碼

在之前的//reset form處呼叫,resetForm()

結合之前的效果:

系統學習iOS動畫之一:檢視動畫

背景中☁️的動畫

如果背景中的那些雲在螢幕上緩慢移動,並從左側移動到右側,然後到右側消失後再左側從新開始緩慢移動,那不是很酷嗎?(之前的gif可以看到雲在移動,到目前為止,雲只有透明度變化動畫,實際上是因為我做GIF時專案已經完成了,GIF是我補做的,所以就。。?)

新增一個animateCloud(cloud: UIImageView)方法,程式碼為:

func animateCloud(cloud: UIImageView) {
    // 假設雲從進入螢幕到離開螢幕需要大約60.0s,可以計算出雲移動的速度
    let cloudSpeed = view.frame.size.width / 60.0
    // 雲的初始位置不一定是在座邊緣
    let duration:CGFloat = (view.frame.size.width - cloud.frame.origin.x) / cloudSpeed
    UIView.animate(withDuration: TimeInterval(duration), delay: 0.0, options: .curveLinear, animations: {
        cloud.frame.origin.x = self.view.frame.size.width
    }) { (_) in
        cloud.frame.origin.x = -cloud.frame.size.width
        self.animateCloud(cloud: cloud)
    }
}
複製程式碼

程式碼解釋:

  1. 首先,計算☁️平均移動速度。假設雲從進入螢幕到離開螢幕需要大約60.0s(當然這個時間自定義)

  2. 接下來,計算☁️移動到螢幕右側的持續時間。這邊要注意,☁️不是從螢幕的左邊緣開始,☁️移動的距離是view.frame.size.width - cloud.frame.origin.x

  3. 然後建立動畫方法animate(withDuration:delay:options:animations:completion:)。這邊TimeIntervalDouble別名,動畫選項使用.curveLinear(不加速也不減速),這種情況很少見,但作為☁️的緩慢移動非常適合。

    動畫閉包中cloud.frame.origin.x = self.view.frame.size.width,就把☁️移動到螢幕右邊區域外。

    到螢幕右區域外,立即在完成閉包中讓☁️到左邊緣外,cloud.frame.origin.x = -cloud.frame.size.width

最後不要忘記,把開始四個☁️的動畫,在viewDidAppear()中新增:

animateCloud(cloud: cloud1)
animateCloud(cloud: cloud2)
animateCloud(cloud: cloud3)
animateCloud(cloud: cloud4)
複製程式碼

整體效果:

整體效果

4-練習檢視動畫

本章是練習之前學習的動畫。

本章節的開始專案 Flight Info 是定時改變幾個檢視(幾個圖片和一個Label),程式碼也非常簡單:

  func changeFlight(to data: FlightData) {
    
    // populate the UI with the next flight's data
    summary.text = data.summary
    flightNr.text = data.flightNr
    gateNr.text = data.gateNr
    departingFrom.text = data.departingFrom
    arrivingTo.text = data.arrivingTo
    flightStatus.text = data.flightStatus
    bgImageView.image = UIImage(named: data.weatherImageName)
    snowView.isHidden = !data.showWeatherEffects
    
    // schedule next flight
    delay(seconds: 3.0) {
      self.changeFlight(to: data.isTakingOff ? parisToRome : londonToParis)
    }
  }
複製程式碼

其中雪花❄️將在後面的章節26-粒子發射器學習,效果為:

開始專案圖示

淡出淡入動畫(Crossfading animations)

首先需要讓兩個背景影像之間平滑過渡。 第一直覺可能是簡單地淡出當前的影像然後淡入新的影像(透明度的變化)。 但是當alpha接近零時,這種方法會顯示影像背後的內容,效果看上去不好。如下所示:

系統學習iOS動畫之一:檢視動畫

ViewController中新增背景圖片淡入淡出的效果:

func fade(imageView: UIImageView, toImage: UIImage, showEffects: Bool) {
    UIView.transition(with: imageView, duration: 1.0, options: .transitionCrossDissolve, animations: {
        imageView.image = toImage
    }, completion: nil)

    UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
        self.snowView.alpha = showEffects ? 1.0 : 0.0
    }, completion: nil)
}
複製程式碼

showEffects參數列示顯示或隱藏降雪效果。

changeFlight方法新增一個是否有動畫的引數animated,並更新changeFlight方法:

func changeFlight(to data: FlightData, animated: Bool = false) {
    summary.text = data.summary
    flightNR.text = data.flightNr
    gateNr.text = data.gateNr
    departingFrom.text = data.departingFrom
    arrivingTo.text = data.arrivingTo
    flightStatus.text = data.flightStatus

    if animated {
        fade(imageView: bgImageView,
             toImage: UIImage(named: data.weatherImageName)!,
             showEffects: data.showWeatherEffects)
    } else {
        bgImageView.image = UIImage(named: data.weatherImageName)
        snowView.isHidden = !data.showWeatherEffects
    }
}
複製程式碼

繼續在changeFlight加一段讓背景圖不停迴圈變換的程式碼:

delay(seconds: 3.0) {
    self.changeFlight(to: data.isTakingOff ? parisToRome : londonToParis, animated: true)
}
複製程式碼

現在的效果是:

系統學習iOS動畫之一:檢視動畫

對比開始時的效果,現在影像之間過渡非常流暢,因為在背景圖淡入淡出的同時也對雪景效果進行了淡入淡出,動畫看起來很無縫。 你甚至可以在羅馬看到它一瞬間下雪!??

不知不覺掌握了一種重要的技術:過渡動畫可用於檢視的不可動畫屬性。1-檢視動畫入門中的可用於動畫的屬性中沒有image

動畫選項.transitionCrossDissolve很適合當前專案的效果,其它如.transitionFlipFromLeft轉換就不大適合,可以試試看。

立體過渡(Cube transitions)

假裝3d轉換時文字背景顏色

系統學習iOS動畫之一:檢視動畫

這不是一個真正的3D效果,但它看起來非常接近。可以通過輔助檢視來實現立體過渡動畫。 具體的方法是新增一個臨時Label,同時對這個兩個標籤的高度進行動畫,最後再刪除。

ViewController中新增一個列舉:

enum AnimationDirection: Int {
    case positive = 1
    case negative = -1
}
複製程式碼

這個列舉的1和-1在之後表示在y軸變換時是向下還是向上。

新增一個cubeTransition方法:

func cubeTransition(label: UILabel, text: String, direction: AnimationDirection) {
    let auxLabel = UILabel(frame: label.frame)
    auxLabel.text = text
    auxLabel.font = label.font
    auxLabel.textAlignment = label.textAlignment
    auxLabel.textColor = label.textColor
    auxLabel.backgroundColor = label.backgroundColor
}
複製程式碼

這是在構造一個臨時輔助的Label,把原來Label屬性複製給它,除了text使用新的值。

在Y軸方向變換輔助Label,向cubeTransition方法中新增:

let auxLabelOffset = CGFloat(direction.rawValue) * label.frame.size.height/2.0
auxLabel.transform = CGAffineTransform(translationX: 0.0, y: auxLabelOffset).scaledBy(x: 1.0, y: 0.1)
label.superview?.addSubview(auxLabel)
複製程式碼

當單獨在Y軸縮放文字時,看起來就像一個豎著的平面被漸漸被推到,從而形成了假遠景效果(faux-perspective effect):

系統學習iOS動畫之一:檢視動畫

動畫程式碼,繼續在cubeTransition方法中新增:

UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: {
    auxLabel.transform = .identity
    // 原本的Label在Y軸上向反方向轉換
    label.transform = CGAffineTransform(translationX: 0.0, y: -auxLabelOffset).scaledBy(x: 1.0, y: 0.1)
},completion: { _ in
    // 把輔助Label的文字賦值給原來的Label,然後刪除輔助Label
    label.text = auxLabel.text
    label.transform = .identity

    auxLabel.removeFromSuperview()
})
複製程式碼

最後要在changeFlight方法中新增這個假的3D轉動效果動畫:

if animated {
    fade(imageView: bgImageView,
         toImage: UIImage(named: data.weatherImageName)!,
         showEffects: data.showWeatherEffects)

    let direction: AnimationDirection = data.isTakingOff ?
    .positive : .negative
    cubeTransition(label: flightNr, text: data.flightNr, direction: direction)
    cubeTransition(label: gateNr, text: data.gateNr, direction: direction)
} else {
    // 不需要動畫
    bgImageView.image = UIImage(named: data.weatherImageName)
    snowView.isHidden = !data.showWeatherEffects

    flightNr.text = data.flightNr
    gateNr.text = data.gateNr

    departingFrom.text = data.departingFrom
    arrivingTo.text = data.arrivingTo

    flightStatus.text = data.flightStatus
}
複製程式碼

最終,航班號和入口號的Label轉換效果(我故意加長了動畫duration,方便觀看):

系統學習iOS動畫之一:檢視動畫

淡入淡出和反彈的過渡

為啟程地和目的地Label新增淡入淡出和反彈的過渡(Fade and bounce transitions)動畫。

先新增方法moveLabel,和上面的類似,建立一個輔助Label,並把原Label的一些屬性複製給它。

func moveLabel(label: UILabel, text: String, offset: CGPoint) {
    let auxLabel = UILabel(frame: label.frame)
    auxLabel.text = text
    auxLabel.font = label.font
    auxLabel.textAlignment = label.textAlignment
    auxLabel.textColor = label.textColor
    auxLabel.backgroundColor = .clear

    auxLabel.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
    auxLabel.alpha = 0
    view.addSubview(auxLabel)
}
複製程式碼

為原Label新增偏移轉換和透明度漸漸降低動畫,在moveLabel方法裡新增:

UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
    label.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
    label.alpha = 0.0
}, completion: nil)
複製程式碼

為輔助Label新增動畫,並在動畫結束後刪除,在moveLabel方法裡新增:

UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseIn, animations: {
    auxLabel.transform = .identity
    auxLabel.alpha = 1.0
}, completion: { _ in
    auxLabel.removeFromSuperview()
    label.text = text
    label.alpha = 1.0
    label.transform = .identity
})
複製程式碼

最後還是在changeFlight方法的if animated {中新增:

// 啟程地和目的地Label動畫
let offsetDeparting = CGPoint(x: CGFloat(direction.rawValue * 80), y: 0.0)
moveLabel(label: departingFrom, text: data.departingFrom, offset: offsetDeparting)
let offsetArriving = CGPoint(x: 0.0, y: CGFloat(direction.rawValue * 50))
moveLabel(label: arrivingTo, text: data.arrivingTo, offset: offsetArriving)
複製程式碼

啟程地和目的地Label動畫的方向可以修改。

效果圖:

系統學習iOS動畫之一:檢視動畫

航班狀態條的動畫

可以使用前面的假的3D轉動效果動畫,changeFlightif animated {中新增:

cubeTransition(label: flightStatus, text: data.flightStatus, direction: direction)

複製程式碼

系統學習iOS動畫之一:檢視動畫

本章節最終的效果:

系統學習iOS動畫之一:檢視動畫

5-關鍵幀動畫

很多時候,需要多個連續的動畫。 前面的章節,已經使用動畫閉包和完成閉包包含兩個動畫效果。

這種方法適用於連線兩個簡單的動畫,但是當我們想要將三個,四個或更多動畫組合在一起時,就會導致一些令人難以置信的混亂和複雜的程式碼。

讓我們看看如果想將多個動畫連結在一起並以矩形模式移動檢視,它會是什麼樣子:

假設實現如下效果:

系統學習iOS動畫之一:檢視動畫

為了達到這個目的,可以將幾個動畫和完成閉包連結起來:

UIView.animate(withDuration: 0.5, 
  animations: {
    view.center.x += 200.0
  }, 
  completion: { _ in
    UIView.animate(withDuration: 0.5, 
      animations: {
        view.center.y += 100.0
      }, 
      completion: { _ in
        UIView.animate(withDuration: 0.5, 
          animations: {
            view.center.x -= 200.0
          }, 
          completion: { _ in
            UIView.animate(withDuration: 0.5, 
              animations: {
                view.center.y -= 100.0
              }
            )
          }
        )
      }
    )
  }
)
```
複製程式碼

看上去複雜繁瑣,這個時候就需要,使用本章節將要學習的關鍵幀動畫(Keyframe Animations),它可以代替上面繁瑣的巢狀。

開始專案使用上一章節的完成專案Flight Info,通過讓✈️“飛機來”,學習關鍵幀動畫。

image-20181013172100870

讓飛機✈️起飛可以分成四個不同階段的動畫(當然具體怎麼分可以視情況而定):

  1. 在跑道上移動

  2. 給✈️一點高度,向上傾斜飛行

  3. 給飛機更大的傾斜和更快的速度,向上傾斜加速飛行

  4. 最後10%時飛機漸漸淡出檢視

完整的動畫可能會讓人難以置信,但將動畫分解為各個階段會使其更易於管理。 一旦為每個階段定義了關鍵幀,就會容易解決問題。

設定關鍵幀動畫

將讓飛機從起始位置起飛,繞圈,然後降落並滑行回到起點。 每次螢幕在航班背景之間切換時,都會執行此動畫。完整的動畫將看起來像這樣:

系統學習iOS動畫之一:檢視動畫

ViewController中新增planeDepart()方法:

func planeDepart() {
  let originalCenter = planeImage.center

  UIView.animateKeyframes(withDuration: 1.5, delay: 0.0,
    animations: {
      //add keyframes
    }, 
    completion: nil
  )
} 
複製程式碼

並在changeFlightif animated {}呼叫planeDepart()

if animated {
    planeDepart()

    ...
複製程式碼

新增第一個keyframe,在上面//add keyframes新增:

UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
    self.planeImage.center.x += 80.0
    self.planeImage.center.y -= 10.0
})
複製程式碼

addKeyframe(withRelativeStartTime:relativeDuration:animations:) 與之前動畫引數設定不同。withRelativeStartTimerelativeDuration都是相對時間百分比,相對於withDuration

使用相對值可以指定keyframe應該持續總時間的一小部分; UIKit獲取每個keyframe的相對持續時間,並自動計算每個keyframe的確切持續時間,為我們節省了大量工作。 上面的程式碼的意思就是從1.5*0.0開始,持續時間1.5*0.25,✈️向右移動80.0,向上移動10.0。

接著上面,新增第二個keyframe:

UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.4, animations: {
    self.planeImage.transform = CGAffineTransform(rotationAngle: -.pi/8)
})
複製程式碼

這一步是讓✈️有個向上傾斜的角度。

接著,新增第三個keyframe:

UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
    self.planeImage.center.x += 100.0
    self.planeImage.center.y -= 50.0
    self.planeImage.alpha = 0.0
})
複製程式碼

這一步在移動同時逐漸消失。

新增第四個keyframe:

UIView.addKeyframe(withRelativeStartTime: 0.51, relativeDuration: 0.01, animations: {
    self.planeImage.transform = .identity
    self.planeImage.center = CGPoint(x: 0.0, y: originalCenter.y)
})
複製程式碼

這一步讓✈️回到與原來高度相同的螢幕左邊緣,不過現在換處於透明度為0狀態。

新增第五個keyframe:

UIView.addKeyframe(withRelativeStartTime: 0.55, relativeDuration: 0.45, animations: {
    self.planeImage.alpha = 1.0
    self.planeImage.center = originalCenter
})
複製程式碼

讓飛機回到原來位置。

現在來看這個五個keyframe的開始時間,它們不是一個接著一個的,而是有交集的,這是因為分步動畫本身就有交叉,✈️在跑道上移動過程中也會有向上移動,機頭也會漸漸向上傾斜,我把每一步的開始和持續時間列出來,得到這個時間可能需要之前不停調節,看什麼時間分隔比較流暢?,下面是比較流暢的時間分隔方式。

(0.0, 0.25)
(0.1, 0.4)
(0.25, 0.25)
(0.51, 0.01)
(0.55, 0.45)
複製程式碼

效果為:

系統學習iOS動畫之一:檢視動畫

關鍵幀動畫中的計算模式

關鍵幀動畫不支援標準檢視動畫中可用的內建動畫緩動。 這是設計好的; 關鍵幀應該在特定時間開始和結束並相互流動。

如果上面動畫的每個階段都有一個緩動曲線,那麼飛機就會抖動,而不是從一個動畫平穩地移動到下一個動畫。

上面沒有提到animateKeyframes(withDuration:delay:options:animations:completion:)方法,這方法有一個options引數(UIViewKeyframeAnimationOptions),可以提供幾種計算模式的選擇,每種模式提供了一種不同的方法來計算動畫的中間幀以及不同的優化器,以實現幀之前的轉化, 有關更多詳細資訊,可檢視文件UIViewKeyframeAnimationOptions

航班出發時間動畫

由於航班出發時間在螢幕頂部,變化時可以簡單先向上移動到螢幕外,然後變化後再向下移動到螢幕內。

func summarySwitch(to summaryText: String) {
    UIView.animateKeyframes(withDuration: 1.0, delay: 0.0, animations: {
        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.45, animations: {
            self.summary.center.y -= 100.0
        })
        UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.45, animations: {
            self.summary.center.y += 100.0
        })
    }, completion: nil)

    delay(seconds: 0.5) {
        self.summary.text = summaryText
    }
}
複製程式碼

同樣在changeFlightif animated {}中呼叫summarySwitch()

本章最後效果:

系統學習iOS動畫之一:檢視動畫

本文在我的個人部落格中地址:系統學習iOS動畫之一:檢視動畫

相關文章