Swift 核心动画扩展封装

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容