Swift 核心動畫 面向協議 擴展封裝

iOS 動畫大可能是用UIView, 複雜一些的須要用到核心動畫,但徹底不一樣風格的使用方式, 和複雜的調用流程實在讓萌新頭疼。git

前幾天用須要作動畫, 用Swift 擴展了核心動畫的庫, 用起來舒服多了.github

不自吹了先看代碼:spring

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, 當動畫總體結束時調用另外一個方法api

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

圓形進度動畫.gif

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

首先,確定是擴展CALayer,添加animate方法, 這裏閉包傳給使用者一個AnimationsMaker動畫構造器 泛型給當前CALayer的實際類型(由於Layer 多是 CATextLayer, CAShareLayer, CAGradientLayer ...等等 他們都繼承自CALayer)bash

這樣咱們就能夠精確的給構造器添加能夠動畫的屬性, 不能動畫的屬性則 . 不出來.閉包

extension CALayer {
    public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
    }
}
複製代碼

想法是好的, 遺憾的是失敗了.app

xcode提示 Self 只能用做返回值 或者協議中,難道就沒辦法解決了嗎?ide

答案是有的

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, 本文爲做者原創, 如需轉載, 請註明出處和原文連接。

相關文章
相關標籤/搜索