【實例教程】你會用swift創建復雜的加載動畫嗎
時至今日,iOS 應用商店已經擁有超過了140萬 應用,讓你自己的應用脫穎而出確實是個不小的挑戰。不過,在你的應用掉入默默無聞的大黑洞之前,你擁有一個小小的機遇窗,它能幫你吸引用戶的注意。
想讓你的用戶喝彩尖叫,沒有比應用加載界面更好的地方 ,在這個地方,你可以添加一個討人喜歡的動畫來作為你登陸或者認證流程的先導。
在這個教程中,你將要學會如何利用先進的技術來創建一個流暢并且迷人的動畫。
開始吧!!
從這里下載啟動項目,保存在一個合適的路徑并用Xcode打開。
打開 HolderView.swift 。 在這個UIView 的子類中,你可以添加些子層(在Layers的下級目錄中可以找到),使之像上面的動畫一樣生動
OvalLayer.swift: 這是第一層,它從零尺寸擴展,然后會有一小段時間的搖擺
TriangleLayer.swift: 接下來的這個層TriangleLayer會在OvalLayer 搖擺的時候出現,當此視圖轉動時,OvalLayer 會縮小到零尺寸,并在TriangleLayer 中消失。
RectangleLayer.swift: 這個層是TriangleLayer 用于分類的可視化容器
ArcLayer.swift: 這個層動畫特效填充在RectangleLayer 中,這和杯子里填充了水(效果)非常相似
打開OvalLayer.swift, 啟動項目已經包含了用于初始化這個層的代碼和所有你會在動畫里用到的Bezier path(對象)。你會看到expand(),wobble()和contract()方法都是空的。你可以通過參考這個指導書來填充這些方法。所有其他的 *Layer (以layer結尾)的文件都用了相似的方式構建。
注意:如果你想要學習更多的Bezier paths,那就檢出我們系列指導書 Modern Core Graphics with Swift
最后,打開ViewController.swift 查看addHolderView()方法,這個方法添加了一個HolderView 作為一個子視圖,放到viewcontroller 視圖的中間。這個視圖將會放置所有的動畫。viewcontroller僅僅需要把它放到屏幕上,這個視圖將會照管好現行的動畫代碼。
animateLabel() 是由類 HolderView 提供的代理回調函數,此類中你會用你完成的動畫序列來填充。addButton()方法只是添加一個按鈕到視圖中,用于觸摸和重啟動畫。
編譯并運行你的應用;你會看到一個空白屏幕。一個空白的畫布--這就是用于開始創建你的新動畫的完美的載體。在指導書的最后,你的應用會看起來是這樣的:
現在不需再費周折,我們開始吧
添加一個橢圓
這個動畫從一個橢圓開始,橢圓是從屏幕中間擴展到視圖,然后在周圍有點搖擺。
打開 HolderView.swift ,在 HolderView 類的頂端附近聲明如下的常量
- let ovalLayer = OvalLayer()
現在在此類的底部添加如下方法
- func addOval() {
- layer.addSublayer(ovalLayer)
- ovalLayer.expand()
- }
這段代碼首先添加了你上面創建的 OverLayer 的實例作為一個子層到視圖層,然后調用 expand() 方法,這是被切掉的需要你來填充的函數之一
來到 OvalLayer.swift 文件,添加如下代碼到 expand() 中:
- func expand() {
- var expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
- expandAnimation.fromValue = ovalPathSmall.CGPath
- expandAnimation.toValue = ovalPathLarge.CGPath
- expandAnimation.duration = animationDuration
- expandAnimation.fillMode = kCAFillModeForwards
- expandAnimation.removedOnCompletion = false
- addAnimation(expandAnimation, forKey: nil)
- }
這個函數創建了一個 CABasicAnimation 的實例,這個實例用于改變橢圓從 ovalPathLarge.到 ovalPathSmall 的路徑。啟動項目為你提供了兩者的Bezier paths。
設置動畫的 removedOnCompletion 的值為 false,fillMode 的值為 KCAFillModeForwards ,使得當動畫結束的時候,橢圓保留它新的路徑。
最后,打開 ViewController.swift ,在 view.addSubview(holderView) 下的 addHolderView() 方法中添加如下的線條
- holderView.addOval()
將 holdview 添加到 ViewController 的視圖中后,調用 addOval 方法來啟動動畫
構建并運行你的應用,你的動畫現在就會看起來像下面(圖例)
搖動橢圓
使用視圖中擴張的橢圓,下一步就是在橢圓的步調中設置一些反彈,使之搖擺起來
打開 HolderView.swift,在此類的底部,添加下面的函數
- func wobbleOval() {
- ovalLayer.wobble()
- }
在 OvalLayer 中調用被切掉的方法 wobble().
現在打開 OverLayer.swift,在 wobble() 中添加如下代碼
- func wobble() {
- // 1
- var wobbleAnimation1: CABasicAnimation = CABasicAnimation(keyPath: "path")
- wobbleAnimation1.fromValue = ovalPathLarge.CGPath
- wobbleAnimation1.toValue = ovalPathSquishVertical.CGPath
- wobbleAnimation1.beginTime = 0.0
- wobbleAnimation1.duration = animationDuration
- // 2
- var wobbleAnimation2: CABasicAnimation = CABasicAnimation(keyPath: "path")
- wobbleAnimation2.fromValue = ovalPathSquishVertical.CGPath
- wobbleAnimation2.toValue = ovalPathSquishHorizontal.CGPath
- wobbleAnimation2.beginTime = wobbleAnimation1.beginTime + wobbleAnimation1.duration
- wobbleAnimation2.duration = animationDuration
- // 3
- var wobbleAnimation3: CABasicAnimation = CABasicAnimation(keyPath: "path")
- wobbleAnimation3.fromValue = ovalPathSquishHorizontal.CGPath
- wobbleAnimation3.toValue = ovalPathSquishVertical.CGPath
- wobbleAnimation3.beginTime = wobbleAnimation2.beginTime + wobbleAnimation2.duration
- wobbleAnimation3.duration = animationDuration
- // 4
- var wobbleAnimation4: CABasicAnimation = CABasicAnimation(keyPath: "path")
- wobbleAnimation4.fromValue = ovalPathSquishVertical.CGPath
- wobbleAnimation4.toValue = ovalPathLarge.CGPath
- wobbleAnimation4.beginTime = wobbleAnimation3.beginTime + wobbleAnimation3.duration
- wobbleAnimation4.duration = animationDuration
- // 5
- var wobbleAnimationGroup: CAAnimationGroup = CAAnimationGroup()
- wobbleAnimationGroup.animations = [wobbleAnimation1, wobbleAnimation2, wobbleAnimation3,
- wobbleAnimation4]
- wobbleAnimationGroup.duration = wobbleAnimation4.beginTime + wobbleAnimation4.duration
- wobbleAnimationGroup.repeatCount = 2
- addAnimation(wobbleAnimationGroup, forKey: nil)
- }
代碼真夠多的。但斷句還是很講究的。 接下來要做的是:
- 從大路徑下降到被垂直壓扁的動畫
- 從垂直壓扁變成水平和垂直都壓扁
- 和垂直擠壓(動畫)切換
- 回到大路徑結束動畫
- 把你所有的動畫合并到CAAnimationGroup組,并把這個動畫組添加到你的 OvalLayout 中。
每一個隨后的動畫的 beginTime 都是其前一個動畫和動畫持續時間的 beginTime 總和。你重復動畫組兩次就會給你一種擺動出稍微拉長的感覺
#p#
盡管你現在擁有產生搖擺動畫的所有代碼,你還是不能調用你的新動畫
我們回到 HolderView.swift,在 addOval() 結尾處添加如下代碼
- NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "wobbleOval",
- userInfo: nil, repeats: false)
在這里,你創建了一個timer定時器,它會在OvalLayer已經結束擴張后調用 wobbleOval()
編譯并運行你的應用,檢查下你的新動畫。
這有點微妙,但那對一個真正的明快的動畫是一個重要的因素。你不再需要那些滿屏幕都是亂飛的東西了。
開始變身
是時候來電有趣的東西了。你將要把一個橢圓變身成為一個三角形。在用戶眼里,這個轉變應該看上去無縫連接的。要做到這些,你會用到兩個相同顏色的分離的形狀。
打開HolderView.swift,在HolderView類的頂端稍微靠近你早些時候添加的 OvalLayer 屬性的下面添加如下代碼
- let triangleLayer = TriangleLayer()
這里聲明了一個 TriangleLayer 類的常量,正如你在 OvalLayer 中做的一樣
現在,讓wobbleOval()方法看上去像這樣:
- func wobbleOval() {
- // 1
- layer.addSublayer(triangleLayer) // Add this line
- ovalLayer.wobble()
- // 2
- // Add the code below
- NSTimer.scheduledTimerWithTimeInterval(0.9, target: self,
- selector: "drawAnimatedTriangle", userInfo: nil,
- repeats: false)
- }
上面的代碼做了如下這些事情:
-
這行(代碼)添加了一個 TiangleLayer 實例,這個實例在稍早的時候作為HolderView層的子層已經被初始化過了。
-
正如你所知道的,因為這個搖擺動畫在1.8s的總間隔時間內運行兩次,所以在中間點啟動變形過程會是一個非常好的地方。因此,你要添加一個定時器timer,它在延遲0.9s之后執行drawAnimatedTriangle()
注意:找到動畫的正確的間隔或延遲需要反復實驗,這也是一個好的動畫和一個極好的動畫區別。我鼓勵你去修補你的動畫,讓它們看上去完美。這可能要花點時間,但確是值得的。
接下來,在此類的底部添加如下的函數。
- func drawAnimatedTriangle() {
- triangleLayer.animate()
- }
這個方法會被你剛剛加入到 wobbleOval() 中的timer定時器調用。
現在打開 TriangleLayer.swift,添加如下代碼到 animate()
- func animate() {
- var triangleAnimationLeft: CABasicAnimation = CABasicAnimation(keyPath: "path")
- triangleAnimationLeft.fromValue = trianglePathSmall.CGPath
- triangleAnimationLeft.toValue = trianglePathLeftExtension.CGPath
- triangleAnimationLeft.beginTime = 0.0
- triangleAnimationLeft.duration = 0.3
- var triangleAnimationRight: CABasicAnimation = CABasicAnimation(keyPath: "path")
- triangleAnimationRight.fromValue = trianglePathLeftExtension.CGPath
- triangleAnimationRight.toValue = trianglePathRightExtension.CGPath
- triangleAnimationRight.beginTime = triangleAnimationLeft.beginTime + triangleAnimationLeft.duration
- triangleAnimationRight.duration = 0.25
- var triangleAnimationTop: CABasicAnimation = CABasicAnimation(keyPath: "path")
- triangleAnimationTop.fromValue = trianglePathRightExtension.CGPath
- triangleAnimationTop.toValue = trianglePathTopExtension.CGPath
- triangleAnimationTop.beginTime = triangleAnimationRight.beginTime + triangleAnimationRight.duration
- triangleAnimationTop.duration = 0.20
- var triangleAnimationGroup: CAAnimationGroup = CAAnimationGroup()
- triangleAnimationGroup.animations = [triangleAnimationLeft, triangleAnimationRight,
- triangleAnimationTop]
- triangleAnimationGroup.duration = triangleAnimationTop.beginTime + triangleAnimationTop.duration
- triangleAnimationGroup.fillMode = kCAFillModeForwards
- triangleAnimationGroup.removedOnCompletion = false
- addAnimation(triangleAnimationGroup, forKey: nil)
- }
這段代碼使三角層TriangleLayer的角一個挨一個的被彈拉成為橢圓 OvalLayer 層的擺動。Bezier path已經作為啟動工程的一部分被定義好。左邊的角首先執行,接下來是右邊的角,最后是上面的。你完成這個(動畫)需要借助創建三個基于路徑的CABasicAnimation類的實例, CABasicAnimation 類已經被你添加到 CAAnimationGroup 組中,而組則被放到了 TriangleLayer 中。
構建并運行你的應用,看看當前動畫的狀態.
完成變形
為了完成變形過程,你需要在縮小OvalLayer橢圓層的同時,對 HolderView 旋轉360度,讓 TriangleLayer 三角層單獨隔離出來。
打開 HolderView.swift,在 drawAnimatedTriangle(): 尾部添加如下代碼
- NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "spinAndTransform",
- userInfo: nil, repeats: false)
這里設置了一個定時器timer,用于在三角形動畫結束后觸發。0.9s的時間還次用反復實驗來確定
現在在這個類的底部添加如下的函數。
- func spinAndTransform() {
- // 1
- layer.anchorPoint = CGPointMake(0.5, 0.6)
- // 2
- var rotationAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
- rotationAnimation.toValue = CGFloat(M_PI * 2.0)
- rotationAnimation.duration = 0.45
- rotationAnimation.removedOnCompletion = true
- layer.addAnimation(rotationAnimation, forKey: nil)
- // 3
- ovalLayer.contract()
- }
你之前創建的定時器添加了這段代碼,定時器會在橢圓停止擺動并且三角行的角出現的時候調用這個函數。在這里我們看下這個函數更詳細的(介紹)
-
更新層的錨點到略微靠近視圖中間的下方。這提供了一個看上去更加自然的旋轉。這是由于橢圓和三角形事實上比視圖中心在垂直方向上略微偏移。因此,如果視圖圍繞中心旋轉,橢圓和三角形可能會垂直方向移動
-
應用一個CABasicAnimation類來對層做360度旋轉,或者2*pi的弧度。旋轉是圍繞著Z軸,Z軸就是穿過屏幕,垂直于屏幕平面的軸
-
在OvalLayer中調用contract()來展示動畫,這個動畫會削減橢圓的尺寸直到消失
現在打開 OvalLayer.swift,添加如下代碼到 contract() 方法
- func contract() {
- var contractAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
- contractAnimation.fromValue = ovalPathLarge.CGPath
- contractAnimation.toValue = ovalPathSmall.CGPath
- contractAnimation.duration = animationDuration
- contractAnimation.fillMode = kCAFillModeForwards
- contractAnimation.removedOnCompletion = false
- addAnimation(contractAnimation, forKey: nil)
- }
這段代碼應用 CABasicAnimation 類,將 OvalLayer 設置它的初始路徑 ovalPathSmall。
構建并運行你的應用程序,當動畫完成的時候,只有三角形應該被留在屏幕上。
繪制容器
在下面這部分,你將要繪畫一個矩形容器,用于創建一個閉合圈。你將會用到 RectangleLayer 的描邊屬性。你需要這樣做兩次,將紅色和藍色都作為描邊色。
打開 HolderView.swift, 像下面這樣聲明兩個 RectangularLayer 常量,(位置)就在你稍早時候 triangleLayer 屬性的下面
- let redRectangleLayer = RectangleLayer()let blueRectangleLayer = RectangleLayer()
接下來添加如下代碼到 spinAndTransform(): 的尾部。
- NSTimer.scheduledTimerWithTimeInterval(0.45, target: self,
- selector: "drawRedAnimatedRectangle",
- userInfo: nil, repeats: false)
- NSTimer.scheduledTimerWithTimeInterval(0.65, target: self,
- selector: "drawBlueAnimatedRectangle",
- userInfo: nil, repeats: false)
這里創建兩個定時器timer分別調用 drawRedAnimatedRectangle() 和 drawBlueAnimatedRectangle() 。旋轉動畫結束后,首先需要畫出矩形,當紅色矩形描邊繪畫接近完成的時候,藍色矩形描邊開始。
添加下面兩個方法頭此類的底部
- func drawRedAnimatedRectangle() {
- layer.addSublayer(redRectangleLayer)
- redRectangleLayer.animateStrokeWithColor(Colors.red)
- }
- func drawBlueAnimatedRectangle() {
- layer.addSublayer(blueRectangleLayer)
- blueRectangleLayer.animateStrokeWithColor(Colors.blue)
- }
一旦你添加矩形層 RectangleLayer 作為 HolderView 的子層,你就要調用 animateStrokeWithColor(color:) 并通過適當的顏色來繪畫出邊線。
現在打開 RectangleLayer.swift, 像下面這樣填充 animateStrokeWithColor(color:)
- func animateStrokeWithColor(color: UIColor) {
- strokeColor = color.CGColor
- var strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
- strokeAnimation.fromValue = 0.0
- strokeAnimation.toValue = 1.0
- strokeAnimation.duration = 0.4
- addAnimation(strokeAnimation, forKey: nil)
- }
這段代碼通過添加一個 CABasicAnimation對象,在 RectangleLayer 矩形層周圍繪畫了一個描邊。CAShapeLayer 的 strokeEnd 的 key(也就是keyPath)指示了在路徑周圍多遠的距離停止描邊。通過將這個屬性值從0調到1,你會產生一種路徑被從開始到結束都被繪畫的錯覺。 而從1到0,將會產生整個路徑被抹去的錯覺。
編譯并運行你的應用,查看兩個描邊是如何看起來像他們構建的容器的。
#p#
填充容器
動畫的下一步就是填充容器。你要尋找到的效果就像是水填充到玻璃杯中。這是個非常棒的視覺特效,使之為一個大的飛濺特效
打開 HolderView.swift,在 RectangleLayer 屬性稍靠下添加如下的常量
- let arcLayer = ArcLayer()
現在在drawBlueAnimatedRectangle():尾部添加如下的代碼
- NSTimer.scheduledTimerWithTimeInterval(0.40, target: self, selector: "drawArc",
- userInfo: nil, repeats: false)
這(段代碼)創建了一個定時器,用于當藍色 RectangleLayer 完成繪畫后調用 drawArc()
在類的結尾添加如下的函數
- func drawArc() {
- layer.addSublayer(arcLayer)
- arcLayer.animate()
- }
這段代碼是在你動畫填充之前,添加了上面已經創建ArcLayer 的實例對象到HolderView 層。
打開ArcLayer.swift 然后添加如下代碼到animate():
- func animate() {
- var arcAnimationPre: CABasicAnimation = CABasicAnimation(keyPath: "path")
- arcAnimationPre.fromValue = arcPathPre.CGPath
- arcAnimationPre.toValue = arcPathStarting.CGPath
- arcAnimationPre.beginTime = 0.0
- arcAnimationPre.duration = animationDuration
- var arcAnimationLow: CABasicAnimation = CABasicAnimation(keyPath: "path")
- arcAnimationLow.fromValue = arcPathStarting.CGPath
- arcAnimationLow.toValue = arcPathLow.CGPath
- arcAnimationLow.beginTime = arcAnimationPre.beginTime + arcAnimationPre.duration
- arcAnimationLow.duration = animationDuration
- var arcAnimationMid: CABasicAnimation = CABasicAnimation(keyPath: "path")
- arcAnimationMid.fromValue = arcPathLow.CGPath
- arcAnimationMid.toValue = arcPathMid.CGPath
- arcAnimationMid.beginTime = arcAnimationLow.beginTime + arcAnimationLow.duration
- arcAnimationMid.duration = animationDuration
- var arcAnimationHigh: CABasicAnimation = CABasicAnimation(keyPath: "path")
- arcAnimationHigh.fromValue = arcPathMid.CGPath
- arcAnimationHigh.toValue = arcPathHigh.CGPath
- arcAnimationHigh.beginTime = arcAnimationMid.beginTime + arcAnimationMid.duration
- arcAnimationHigh.duration = animationDuration
- var arcAnimationComplete: CABasicAnimation = CABasicAnimation(keyPath: "path")
- arcAnimationComplete.fromValue = arcPathHigh.CGPath
- arcAnimationComplete.toValue = arcPathComplete.CGPath
- arcAnimationComplete.beginTime = arcAnimationHigh.beginTime + arcAnimationHigh.duration
- arcAnimationComplete.duration = animationDuration
- var arcAnimationGroup: CAAnimationGroup = CAAnimationGroup()
- arcAnimationGroup.animations = [arcAnimationPre, arcAnimationLow, arcAnimationMid,
- arcAnimationHigh, arcAnimationComplete]
- arcAnimationGroup.duration = arcAnimationComplete.beginTime + arcAnimationComplete.duration
- arcAnimationGroup.fillMode = kCAFillModeForwards
- arcAnimationGroup.removedOnCompletion = false
- addAnimation(arcAnimationGroup, forKey: nil)
- }
這個動畫和之前的搖擺動畫很相似。你創建了一個 CAAnimationGroup 動畫組,動畫組中包含五個基于路徑的 CABasicAnimation 實例對象。
每個路徑因高度遞增而有了稍微不同的弧,這些路徑也是啟動項目的一部分。最后,將 CAAnimationGroup 動畫組應用到層中,并使得動畫組在完成的時候不會被移除,因而當動畫完成的時候,它依然保留了自己的狀態。
構建并運行你的應用,看看這個神奇的展開吧。
完成動畫
剩下要做的就是擴展藍色的HolderView視圖來填充整個屏幕,并且添加一個UILabel作為一個logo添加到視圖中
打開 HolderView.swift,在drawArc() 的結尾添加如下代碼
- NSTimer.scheduledTimerWithTimeInterval(0.90, target: self, selector: "expandView",
- userInfo: nil, repeats: false)
這(段代碼)創建了一個定時器,用于在 ArcLayer 填充到容器后調用 expandView()
現在,添加下面的函數到同一個類的底部:
- func expandView() {
- // 1
- backgroundColor = Colors.blue
- // 2
- frame = CGRectMake(frame.origin.x - blueRectangleLayer.lineWidth,
- frame.origin.y - blueRectangleLayer.lineWidth,
- frame.size.width + blueRectangleLayer.lineWidth * 2,
- frame.size.height + blueRectangleLayer.lineWidth * 2)
- // 3
- layer.sublayers = nil
- // 4
- UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut,
- animations: {
- self.frame = self.parentFrame
- }, completion: { finished in
- self.addLabel()
- })
- }
代碼分析
-
HolderView視圖的背景設置為藍色,和你填充到矩形的顏色匹配
-
幀擴展到你稍早時候添加的RectangleLayer矩形層的描邊寬度,
-
所有的子層都移除。現在沒有了橢圓,沒有了三角形,沒有了矩形圖層
-
添加動畫,并擴張HolderView填充屏幕,當動畫結束的時候,調用addLabel().
在類的底部,添加如下函數
- func addLabel() {
- delegate?.animateLabel()
- }
這里只是簡單的調用視圖的代理函數,展示label標簽。
現在打開ViewController.swift,添加如下代碼到animateLabel():
- func animateLabel() {
- // 1
- holderView.removeFromSuperview()
- view.backgroundColor = Colors.blue
- // 2
- var label: UILabel = UILabel(frame: view.frame)
- label.textColor = Colors.white
- label.font = UIFont(name: "HelveticaNeue-Thin", size: 170.0)
- label.textAlignment = NSTextAlignment.Center
- label.text = "S"
- label.transform = CGAffineTransformScale(label.transform, 0.25, 0.25)
- view.addSubview(label)
- // 3
- UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.1, options: UIViewAnimationOptions.CurveEaseInOut,
- animations: ({
- label.transform = CGAffineTransformScale(label.transform, 4.0, 4.0)
- }), completion: { finished in
- self.addButton()
- })
- }
依次帶入各個注釋段
-
從視圖中移除HolderView ,并設置視圖的背景顏色為藍色。
-
創建一個文本為"S"的UIlabel標簽對象,用于展示logo,并添加到視圖。
-
標簽對象使用一個彈性動畫來使之伸縮。一旦動畫結束,調用 addButton() 來添加一個按鈕到視圖中,當按鈕按下的時候,重復動畫。
構建并運行應用程序,給自己點個贊,花個時間來欣賞自己構建的動畫吧。
下一步
你可以從這里 下載 最終完整的項目。
這個指導書包含了相當多的不一樣的動畫技術,當這些動畫都堆疊在一起的時候,能夠創造一個相當復雜的加載動畫,這確實能夠讓你的應用在第一次被(用戶)運行的時候就眼前一亮。
從這里,放松自由的玩玩不一樣的(動畫 的)定時和形狀,看看你能組裝成哪些很酷的動畫
如果你想讓你新發現的動畫技術提升一個檔次,那我建議你看下我們的(這本)書iOS Animations by Tutorials.
我希望通過這本指導書你能得到極大的樂趣,并且,如果你有任何問題或者建議,請加入我們下面的論壇討論吧