iOS Animation 指北
寫在前面
iOS 向來以絲般順滑的過度動畫聞名,好的動畫可以讓用戶更好地理解 app,并且可以讓 app 更加有趣。有趣很重要。
iOS 動畫(或者所有動畫?)的原理簡單來講有兩種:
- 告訴系統動畫對象在某幾個時刻的狀態(關鍵幀),由系統自動補全這些時刻之間的中間狀態,再把所有這些狀態平滑地顯示出來。這種動畫也叫做關鍵幀動畫。
- 每隔一段很短的時間,重新繪制一次動畫對象。
整體架構
這里是一張 iOS 動畫相關 framework 的架構圖:
從圖中可以看到,要實現一個 iOS 動畫從上層到底層你會接觸到 UIKit、Core Animation、Core Graphics/OpenGL。他們使用的難度依次遞增,相應地靈活程度以及能實現效果的復雜程度也越來越高。
基礎知識
CALayer
CALayer 是 UIView 背后用來管理顯示內容的對象,它維護了一個位圖以及位圖的狀態信息。這個位圖可能是開發者用程序繪制的(比如在 drawRect 中),也可能是開發者指定的一張圖片。
Core Animation 的所有功能都是基于 layer 的。當你通過 Core Animation 修改了 layer 的屬性,Core Animation 會通過 GPU 來重新渲染位圖。
利用 CALayer 的屬性可以做很多事情,比如改變位置、大小、形狀、透視角度、圓角、透明度等,利用 mask 屬性還可以做出很多好玩的效果。這些屬性的詳細介紹在 這里 。
CALayer 有很多有用的子類,常用的有下面幾個:
- CAShapeLayer:用于繪制曲線等圖形,有兩個重要的屬性 strokeStart & strokeEnd ,靈活使用有奇效。
- CATextLayer:專門用來處理文字的 layer。
- CAGradientLayer:顧名思義,用來處理漸變。
Timing function
Timing function 用來描繪動畫完成度隨時間增加的曲線。
怎么理解這個含義呢,在前面提到的第二種動畫實現方式中,我們設置好了關鍵幀后,系統需要計算出關鍵幀中間各個時間點的狀態。
假設我們要完成的是一個物體沿直線從 (0,0) 移動到 (0, 100) 的動畫,動畫時長是 1s。系統可以以勻速將物體從起點移動到終點,也可以加速或者減速,甚至可以先加速再減速移動到終點,timing function 就是用來描繪這里的加減速。
看看 iOS 預定義的幾種常見的 timing function:
let kCAMediaTimingFunctionLinear: String
let kCAMediaTimingFunctionEaseIn: String
let kCAMediaTimingFunctionEaseOut: String
let kCAMediaTimingFunctionEaseInEaseOut: String
let kCAMediaTimingFunctionDefault: String
可以看到,這里的橫坐標是時間,縱坐標可以看做動畫的完成度。
正如命名描繪的那樣, kCAMediaTimingFunctionLinear 是線性增加的,而 kCAMediaTimingFunctionEaseOut 是先快后慢的。
你甚至還可以通過 init(controlPoints:_:_:_:) 方法,傳入兩個貝塞爾曲線的控制點來自定義想要的曲線。
這樣做的意義是什么呢?因為人們在日常生活中見到的物體的運動幾乎沒有勻速運動的,比如汽車啟動和剎車,比如杯子從桌子上掉落。合理運用 timing function 可以使動畫更符合人們的經驗,因而顯得更加自然。
UIKit
UIKit 提供了一系列的基于 block 的 API。比如 UIView.animate() 系列方法。對于 view 的 animatable properties , 你只需要在 block 中修改需要對應的參數即可,比如 frame、bounds、alpha 等。
如果想實現關鍵幀動畫,UIKit 還提供了 UIView.animateKeyframes() 方法。
UIKit 適用于簡單的動畫,如移動、旋轉、縮放、改變顏色等。
Core Animation
UIKit 實現動畫適用起來非常簡單,但是有很大的局限性:如果我想改變 cornerRadius 怎么辦?如果我想沿一條曲線移動一個 view 怎么辦?
這個時候就需要使用 Core Animation 了(UIKit 其實也是在 Core Animation 的基礎上做了一層封裝)。
Core Animation 分為以下幾個部分:
CABasicAnimation
相比于 UIKit,CABasicAnimation 可以對更多的 CALayer 屬性進行動畫,比如 cornerRadius。完整的列表見 這里 。
CABasicAnimation 使用起來非常方便:
let verticalAnimation = CABasicAnimation(keyPath: "position.y") // 1
verticalAnimation.fromValue = 310 // 2
verticalAnimation.toValue = 10 // 3
verticalAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) // 4
view.layer.add(verticalAnimation, forKey: "Fall") // 5
view.layer.position.y = 10 //6
</code></pre>
簡單解釋一下:
- 這里我們創建了一個 CABasicAnimation 實例,要對 layer 的 position.y 屬性進行動畫。
- position.y 的初始值是 310。
- position.y 的最終值是 10。
- 指定了動畫的 timing function,后面會介紹。
- 將動畫添加到 view 的 layer 上。
- 將 layer 的 model layer 屬性更改為最終屬性。
什么是 model layer?
Core Animation 實際維護了三個 layer:model layer、presentation layer 和 render layer,其中的前兩個我們平時會接觸到,可以分別使用 CALayer 的兩個屬性 modelLayer 和 presentationLayer 來獲得。Render layer 是系統私有的。
Model layer 的屬性是不會變化的,如果你想得到 layer 在動畫過程中實時的屬性,就需要通過 presentation layer 來獲取。
CAKeyframeAnimation
CABasicAnimation 只能讓你設置一個初始狀態和一個結束狀態,如果你的動畫需要拆解成幾個連貫的動作,CAKeyframeAnimation 可以傳入多個不同的值。
此外,CAKeyframeAnimation 還可以設置 CGPath,也就是說你可以讓動畫對象沿著曲線移動。
let positionAnimation = CAKeyframeAnimation(keyPath: "position")
positionAnimation.path = path.cgPath
positionAnimation.isRemovedOnCompletion = false
positionAnimation.fillMode = kCAFillModeForwards
view.layer.add(positionAnimation, forKey: "MoveAlongPath")
</code></pre>
CATransition
CATransition 用來進行 view 的轉場動畫,具體類型有以下四種:
let kCATransitionFade: String
let kCATransitionMoveIn: String
let kCATransitionPush: String
let kCATransitionReveal: String
還有一些私有的類型,不推薦使用。
CATransition 是 CAAnimation 的子類,使用方法跟 CABasicAnimation 一致。
CAAnimationGroup
如果我想讓物體在移動的同時由不透明動畫到透明,就可以使用 CAAnimationGroup,把多個動畫組合起來,同時添加到動畫對象上。
let animationA = ...
let animationB = ...
let animationC = ...
let animationGroup = CAAnimationGroup()
animationGroup.animations = [animationA, animationB, animationC];
animationGroup.duration = 0.7
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = kCAFillModeForwards
view.layer.add(animationGroup, forKey: "GroupAnimation")
</code></pre>
此外,CAAnimationGroup 還可以設置一個 completion block,在所有動畫完成時調用。
Core Animation 適用于較為復雜的,有多個中間狀態或者包含曲線路徑的動畫。
Core Graphics
如果動畫復雜到不能夠用改變位置、透明度、大小等屬性的組合來完成,就需要使用 Core Graphics 了。
Core Graphics 是一套用來繪圖的框架,你可以繪制曲線、填充形狀,做任何想做的事情。這個時候采用的就是前面提到的第二種動畫方法:每隔一段很短的時間,重新繪制一次動畫對象。
注意,采用 Core Graphics 繪制圖形是非常消耗性能的,因為繪制工作由 CPU 完成,而且是在主線程上!如果是簡單的圖形可以使用 CAShapeLayer ,利用 GPU 繪制。
如何來保證「每隔一段很短的時間」呢?iOS 為此提供了 CADisplayLink。它可以被看作是一個特殊的 timer,在系統刷新每一幀的時候,調用開發者設置的回調來重新繪制動畫對象。iOS 屏幕的刷新頻率是 60幀每秒,也就是每隔約 16.7 毫秒調用一次回調。這也意味著,動畫中每一幀的繪制都不應該超過 16.7 毫秒。
那是不是用 NSTimer 也可以達到目的?
并不是的。CADisplayLink 可以保證每次都是在 屏幕刷新的時刻附近 來調用回調——也就是說,你的每一幀都有約 16.7 毫秒來繪制。NSTimer 不能保證觸發時刻都落在屏幕刷新的時刻附近,有可能你的一幀只有 2 毫秒來繪制。
Core Graphics + CADisplayLink 適用于復雜的,不能使用移動、旋轉、形變組合完成的動畫。
大殺器 UIKit Dynamics
UIKit Dynamics 是隨 iOS 7 推出的一套 framework,作用是模擬真實事件的物理定律。蘋果聲稱你可以「聲明式」地編寫動畫,你只需要描述要做什么,而不必說明怎么做,一切都由系統幫你完成。
Ref
來自:http://nixwang.com/2017/02/13/ios-animation/