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, 本文为做者原创, 如需转载, 请注明出处和原文连接。

相关文章
相关标签/搜索