iOS 動畫大多是用UIView
, 複雜一些的需要用到核心動畫,但完全不同風格的使用方式, 和複雜的呼叫流程實在讓萌新頭疼。
前幾天用需要做動畫, 用Swift
擴充套件了核心動畫的庫, 用起來舒服多了.
不自吹了先看程式碼:
view.layer.animate(forKey: "cornerRadius") {
$0.cornerRadius
.value(from: 0, to: cornerValue, duration: animDuration)
$0.size
.value(from: startRect.size, to: endRect.size, duration: animDuration)
$0.position
.value(from: startRect.center, to: endRect.center, duration: animDuration)
$0.shadowOpacity
.value(from: 0, to: 0.8, duration: animDuration)
$0.shadowColor
.value(from: .blackClean, to: color, duration: animDuration)
$0.timingFunction(.easeOut).onStoped {
[weak self] (finished:Bool) in
if finished { self?.updateAnimations() }
}
}
複製程式碼
上面的程式碼中將一個檢視的圓角, 尺寸, 位置, 陰影和陰影顏色都進行了動畫, 並統一設定變化模式為easeOut, 當動畫整體結束時呼叫另一個方法
shareLayer.animate(forKey: "state") {
$0.strokeStart
.value(from: 0, to: 1, duration: 1).delay(0.5)
$0.strokeEnd
.value(from: 0, to: 1, duration: 1)
$0.timingFunction(.easeInOut)
$0.repeat(count: .greatestFiniteMagnitude)
}
複製程式碼
形狀 CAShareLayer
(實際為圓)的 圓形進度條動畫,效果如下
那麼,這些是如何實現的呢?
首先,肯定是擴充套件CALayer
,新增animate
方法, 這裡閉包傳給使用者一個AnimationsMaker
動畫構造器 泛型給當前CALayer的實際型別(因為Layer
可能是 CATextLayer
, CAShareLayer
, CAGradientLayer
…等等 他們都繼承自CALayer)
這樣我們就可以精確的給構造器新增可以動畫的屬性, 不能動畫的屬性則 .
不出來.
extension CALayer {
public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
}
}
複製程式碼
想法是好的, 遺憾的是失敗了.
xcode提示 Self
只能用作返回值 或者協議中,難道就沒辦法解決了嗎?
答案是有的
CALayer
繼承自 CAMediaTiming
協議,那麼我們只需要擴充套件這個協議, 並加上必須繼承自CALayer
的條件, 效果和直接擴充套件CALayer一樣.
extension CAMediaTiming where Self : CALayer {
public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
}
}
複製程式碼
OK 效果完美, 圓滿成功, 但如果一個class 沒實現xxx協議怎麼辦? 這一招還有效麼?
答案是有的
寫一個空協議, 擴充套件目標class 實現此協議, 再擴充套件空協議, 條件是必須繼承自此class , 然後新增方法。
一不小心跑題了,下一步要建立動畫構造器
open class AnimationsMaker<Layer> : AnimationBasic<CAAnimationGroup, CGFloat> where Layer : CALayer {
public let layer:Layer
public init(layer:Layer) {
self.layer = layer
super.init(CAAnimationGroup())
}
internal var animations:[CAAnimation] = []
open func append(_ animation:CAAnimation) {
animations.append(animation)
}
internal var _duration:CFTimeInterval?
/* The basic duration of the object. Defaults to 0. */
@discardableResult
open func duration(_ value:CFTimeInterval) -> Self {
_duration = value
return self
}
}
複製程式碼
目的很明顯, 就是建立一個核心動畫的組, 以方便於將後面一堆屬性動畫合併成一個
下面開始完善之前的方法
extension CAMediaTiming where Self : CALayer {
/// CALayer 建立動畫構造器
public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
// 移除同 key 的未執行完的動畫
if let idefiniter = key {
removeAnimation(forKey: idefiniter)
}
// 建立動畫構造器 並 開始構造動畫
let maker = AnimationsMaker<Self>(layer: self)
makerFunc(maker)
// 如果只有一個屬性做了動畫, 則忽略動畫組
if maker.animations.count == 1 {
return add(maker.animations.first!, forKey: key)
}
// 建立動畫組
let group = maker.caAnimation
group.animations = maker.animations
// 如果未設定動畫時間, 則採用所有動畫中最長的時間做動畫時間
group.duration = maker._duration ?? maker.animations.reduce(0) { max($0, $1.duration + $1.beginTime) }
// 開始執行動畫
add(group, forKey: key)
}
}
複製程式碼
接下來自然是給 動畫構造器 新增CALayer
各種可動畫的屬性
extension AnimationsMaker {
/// 對 cornerRadius 屬性進行動畫 預設 0
public var cornerRadius:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:self, keyPath:"cornerRadius")
}
/// 對 bounds 屬性進行動畫.
public var bounds:AnimationMaker<Layer, CGRect> {
return AnimationMaker<Layer, CGRect>(maker:self, keyPath:"bounds")
}
/// 對 size 屬性進行動畫
public var size:AnimationMaker<Layer, CGSize> {
return AnimationMaker<Layer, CGSize>(maker:self, keyPath:"bounds.size")
}
/// 以下若干屬性略
......
}
複製程式碼
這裡的AnimationMaker
和 前面的 AnimationsMaker
很像,但其意義是單一屬性的動畫構造器
CABasicAnimation
裡面的fromValue
和toValue
的屬性都是Any?
原因是對layer的不同屬性進行動畫時, 給的值型別也是不確定的, 比如size
屬性 是CGSize
, position
屬性是CGPoint
, zPosition
屬性是CGFloat等, 因此它也只能是Any?
。
但這不符合Swift 安全語言的目標, 因為我們使用時可能不小心傳遞了一個錯誤的型別給它而不被編譯器發現, 增加了DEBUG的時間, 不利於生產效率
因此, 在定義 AnimationMaker
(單一屬性動畫)時,應使用泛型約束變化的值和動畫屬性值的型別相同,並且為了方便自身構造的CAAnimation
加到動畫組中, 將AnimationsMaker
也傳遞進去
public class AnimationMaker<Layer, Value> where Layer : CALayer {
public unowned let maker:AnimationsMaker<Layer>
public let keyPath:String
public init(maker:AnimationsMaker<Layer>, keyPath:String) {
self.maker = maker
self.keyPath = keyPath
}
/// 指定彈簧係數的 彈性動畫
@available(iOS 9.0, *)
func animate(duration:TimeInterval, damping:CGFloat, from begin:Any?, to over:Any?) -> Animation<CASpringAnimation, Value> {
let anim = CASpringAnimation(keyPath: keyPath)
anim.damping = damping
anim.fromValue = begin
anim.toValue = over
anim.duration = duration
maker.append(anim)
return Animation<CASpringAnimation, Value>(anim)
}
/// 指定起始和結束值的 基礎動畫
func animate(duration:TimeInterval, from begin:Any?, to over:Any?) -> Animation<CABasicAnimation, Value> {
let anim = CABasicAnimation(keyPath: keyPath)
anim.fromValue = begin
anim.toValue = over
anim.duration = duration
maker.append(anim)
return Animation<CABasicAnimation, Value>(anim)
}
/// 指定關鍵值的幀動畫
func animate(duration:TimeInterval, values:[Value]) -> Animation<CAKeyframeAnimation, Value> {
let anim = CAKeyframeAnimation(keyPath: keyPath)
anim.values = values
anim.duration = duration
maker.append(anim)
return Animation<CAKeyframeAnimation, Value>(anim)
}
/// 指定引導線的幀動畫
func animate(duration:TimeInterval, path:CGPath) -> Animation<CAKeyframeAnimation, Value> {
let anim = CAKeyframeAnimation(keyPath: keyPath)
anim.path = path
anim.duration = duration
maker.append(anim)
return Animation<CAKeyframeAnimation, Value>(anim)
}
}
複製程式碼
為了避免可能存在的迴圈引用記憶體洩露, 這裡將父動畫組maker
設為不增加引用計數的 unowned
(相當於OC
的 assign
)
雖然實際上沒迴圈引用, 但因為都是臨時變數, 沒必要增加引用計數, 可以加快執行效率
AnimationMaker
裡只給了動畫必要的基礎屬性, 一些額外屬性可以通過鏈式語法額外設定, 所以返回了一個包裝CAAnimation
的 Animation
物件, 同樣傳遞值型別的泛型
public final class Animation<T, Value> : AnimationBasic<T, Value> where T : CAAnimation {
/* The basic duration of the object. Defaults to 0. */
@discardableResult
public func duration(_ value:CFTimeInterval) -> Self {
caAnimation.duration = value
return self
}
}
複製程式碼
因為CAAnimation
動畫 和CAAnimationGroup
動畫組都共有一些屬性, 所以寫了一個 基類 AnimationBasic
而動畫組的時間額外處理, 預設不給的時候使用所有動畫中最大的那個時間, 否則使用強制指定的時間,參考前面的AnimationsMaker
定義
open class AnimationBasic<T, Value> where T : CAAnimation {
open let caAnimation:T
public init(_ caAnimation:T) {
self.caAnimation = caAnimation
}
/* The begin time of the object, in relation to its parent object, if
* applicable. Defaults to 0. */
@discardableResult
public func delay(_ value:TimeInterval) -> Self {
caAnimation.beginTime = value
return self
}
/* A timing function defining the pacing of the animation. Defaults to
* nil indicating linear pacing. */
@discardableResult
open func timingFunction(_ value:CAMediaTimingFunction) -> Self {
caAnimation.timingFunction = value
return self
}
/* When true, the animation is removed from the render tree once its
* active duration has passed. Defaults to YES. */
@discardableResult
open func removedOnCompletion(_ value:Bool) -> Self {
caAnimation.isRemovedOnCompletion = value
return self
}
@discardableResult
open func onStoped(_ completion: @escaping @convention(block) (Bool) -> Void) -> Self {
if let delegate = caAnimation.delegate as? AnimationDelegate {
delegate.onStoped = completion
} else {
caAnimation.delegate = AnimationDelegate(completion)
}
return self
}
@discardableResult
open func onDidStart(_ started: @escaping @convention(block) () -> Void) -> Self {
if let delegate = caAnimation.delegate as? AnimationDelegate {
delegate.onDidStart = started
} else {
caAnimation.delegate = AnimationDelegate(started)
}
return self
}
/* The rate of the layer. Used to scale parent time to local time, e.g.
* if rate is 2, local time progresses twice as fast as parent time.
* Defaults to 1. */
@discardableResult
open func speed(_ value:Float) -> Self {
caAnimation.speed = value
return self
}
/* Additional offset in active local time. i.e. to convert from parent
* time tp to active local time t: t = (tp - begin) * speed + offset.
* One use of this is to "pause" a layer by setting `speed` to zero and
* `offset` to a suitable value. Defaults to 0. */
@discardableResult
open func time(offset:CFTimeInterval) -> Self {
caAnimation.timeOffset = offset
return self
}
/* The repeat count of the object. May be fractional. Defaults to 0. */
@discardableResult
open func `repeat`(count:Float) -> Self {
caAnimation.repeatCount = count
return self
}
/* The repeat duration of the object. Defaults to 0. */
@discardableResult
open func `repeat`(duration:CFTimeInterval) -> Self {
caAnimation.repeatDuration = duration
return self
}
/* When true, the object plays backwards after playing forwards. Defaults
* to NO. */
@discardableResult
open func autoreverses(_ value:Bool) -> Self {
caAnimation.autoreverses = value
return self
}
/* Defines how the timed object behaves outside its active duration.
* Local time may be clamped to either end of the active duration, or
* the element may be removed from the presentation. The legal values
* are `backwards`, `forwards`, `both` and `removed`. Defaults to
* `removed`. */
@discardableResult
open func fill(mode:AnimationFillMode) -> Self {
caAnimation.fillMode = mode.rawValue
return self
}
}
複製程式碼
接下來開始錦上添花 給單一屬性動畫 新增快速建立
extension AnimationMaker {
/// 建立 指定變化值的幀動畫 並執行 duration 秒的彈性動畫
@discardableResult
public func values(_ values:[Value], duration:TimeInterval) -> Animation<CAKeyframeAnimation, Value> {
return animate(duration: duration, values: values)
}
/// 建立從 begin 到 over 並執行 duration 秒的彈性動畫
@available(iOS 9.0, *)
@discardableResult
public func value(from begin:Value, to over:Value, damping:CGFloat, duration:TimeInterval) -> Animation<CASpringAnimation, Value> {
return animate(duration: duration, damping:damping, from: begin, to: over)
}
/// 建立從 begin 到 over 並執行 duration 秒的動畫
@discardableResult
public func value(from begin:Value, to over:Value, duration:TimeInterval) -> Animation<CABasicAnimation, Value> {
return animate(duration: duration, from: begin, to: over)
}
/// 建立從 當前已動畫到的值 更新到 over 並執行 duration 秒的動畫
@discardableResult
public func value(to over:Value, duration:TimeInterval) -> Animation<CABasicAnimation, Value> {
let begin = maker.layer.presentation()?.value(forKeyPath: keyPath) ?? maker.layer.value(forKeyPath: keyPath)
return animate(duration: duration, from: begin, to: over)
}
}
複製程式碼
給不同的核心動畫新增其獨有屬性
extension Animation where T : CABasicAnimation {
@discardableResult
public func from(_ value:Value) -> Self {
caAnimation.fromValue = value
return self
}
@discardableResult
public func to(_ value:Value) -> Self {
caAnimation.toValue = value
return self
}
/* - `byValue` non-nil. Interpolates between the layer`s current value
* of the property in the render tree and that plus `byValue`. */
@discardableResult
public func by(_ value:Value) -> Self {
caAnimation.byValue = value
return self
}
}
複製程式碼
@available(iOSApplicationExtension 9.0, *)
extension Animation where T : CASpringAnimation {
/* The mass of the object attached to the end of the spring. Must be greater
than 0. Defaults to one. */
/// 質量 預設1 必須>0 越重回彈越大
@available(iOS 9.0, *)
@discardableResult
public func mass(_ value:CGFloat) -> Self {
caAnimation.mass = value
return self
}
/* The spring stiffness coefficient. Must be greater than 0.
* Defaults to 100. */
/// 彈簧鋼度係數 預設100 必須>0 越小回彈越大
@available(iOS 9.0, *)
@discardableResult
public func stiffness(_ value:CGFloat) -> Self {
caAnimation.stiffness = value
return self
}
/* The damping coefficient. Must be greater than or equal to 0.
* Defaults to 10. */
/// 阻尼 預設10 必須>=0
@available(iOS 9.0, *)
@discardableResult
public func damping(_ value:CGFloat) -> Self {
caAnimation.damping = value
return self
}
/* The initial velocity of the object attached to the spring. Defaults
* to zero, which represents an unmoving object. Negative values
* represent the object moving away from the spring attachment point,
* positive values represent the object moving towards the spring
* attachment point. */
/// 初速度 預設 0, 正數表示正方向的初速度, 負數表示反方向的初速度
@available(iOS 9.0, *)
@discardableResult
public func initialVelocity(_ value:CGFloat) -> Self {
caAnimation.initialVelocity = value
return self
}
}
複製程式碼
還有一些略
最後, 給一些特殊屬性, 可以點出子屬性的做一些擴充套件新增
extension AnimationMaker where Value == CGSize {
/// 對 size 的 width 屬性進行動畫
public var width:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"(keyPath).width")
}
/// 對 size 的 height 屬性進行動畫
public var height:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"(keyPath).height")
}
}
複製程式碼
extension AnimationMaker where Value == CATransform3D {
/// 對 transform 的 translation 屬性進行動畫
public var translation:UnknowMaker<Layer, CGAffineTransform> {
return UnknowMaker<Layer, CGAffineTransform>(maker:maker, keyPath:"(keyPath).translation")
}
/// 對 transform 的 rotation 屬性進行動畫
public var rotation:UnknowMaker<Layer, CGAffineTransform> {
return UnknowMaker<Layer, CGAffineTransform>(maker:maker, keyPath:"(keyPath).rotation")
}
}
複製程式碼
extension UnknowMaker where Value == CGAffineTransform {
/// 對 transform 的 x 屬性進行動畫
public var x:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"(keyPath).x")
}
/// 對 transform 的 y 屬性進行動畫
public var y:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"(keyPath).y")
}
/// 對 transform 的 z 屬性進行動畫
public var z:AnimationMaker<Layer, CGFloat> {
return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"(keyPath).z")
}
}
複製程式碼
暫時沒有深入瞭解 transform
更多屬性的動畫, 因此只寫了幾個已知的基礎屬性, 為了避免中間使用異常, 所以弄了個 UnknowMaker
對此熟悉的大佬可以幫忙補充。
最後擴充套件了2個常用範例
private let kShakeAnimation:String = "shakeAnimation"
private let kShockAnimation:String = "shockAnimation"
extension CALayer {
/// 搖晃動畫
public func animateShake(count:Float = 3) {
let distance:CGFloat = 0.08 // 搖晃幅度
animate(forKey: kShakeAnimation) {
$0.transform.rotation.z
.value(from: distance, to: -distance, duration: 0.1).by(0.003)
.autoreverses(true).repeat(count: count).timingFunction(.easeInOut)
}
}
/// 震盪動畫
public func animateShock(count:Float = 2) {
let distance:CGFloat = 10 // 震盪幅度
animate(forKey: kShockAnimation) {
$0.transform.translation.x
.values([0, -distance, distance, 0], duration: 0.15)
.autoreverses(true).repeat(count: count).timingFunction(.easeInOut)
}
}
}
複製程式碼
最後為了方便使用, 減少編譯時間, 將專案寫成了一個庫, iOS 和 Mac 都可以用, 因為Swift 4 仍然沒有穩定ABI的庫, 建議將庫拖入專案 使用
記的不僅僅是Linked Frameworks
自定義的framework 都要加入 Embedded Binaries
如果好用請給我個Start, 本文為作者原創, 如需轉載, 請註明出處和原文連結。