Swift 核心動畫 面向協議 擴充套件封裝

小歪發表於2019-01-31

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 (實際為圓)的 圓形進度條動畫,效果如下

圓形進度動畫.gif

那麼,這些是如何實現的呢?

首先,肯定是擴充套件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 裡面的fromValuetoValue 的屬性都是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 (相當於OCassign)

雖然實際上沒迴圈引用, 但因為都是臨時變數, 沒必要增加引用計數, 可以加快執行效率

AnimationMaker裡只給了動畫必要的基礎屬性, 一些額外屬性可以通過鏈式語法額外設定, 所以返回了一個包裝CAAnimationAnimation 物件, 同樣傳遞值型別的泛型

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的庫, 建議將庫拖入專案 使用

WX20171207-105559@2x.png

記的不僅僅是Linked Frameworks 自定義的framework 都要加入 Embedded Binaries

原始碼 Github下載地址

如果好用請給我個Start, 本文為作者原創, 如需轉載, 請註明出處和原文連結。

相關文章