jiangleligejiang / JNote

记录相关日常笔记
0 stars 0 forks source link

EasyAnimation源码探究 #46

Open jiangleligejiang opened 5 years ago

jiangleligejiang commented 5 years ago

一、EasyAnimation简介

简而言之,EasyAnimation让你可以使用UIView.animate系统方法便捷地修改动画属性,且支持链式调用和取消动画,以及兼容iOS 8上使用Spring动画效果。

EasyAnimation (after) UIView.animate(duration: 2.0, animations: { self.view.layer.position.x = 200.0 })


- iOS8上也支持`Spring Layer Animations`

```swift
UIView.animate(duration: 2.0, delay: 0.0, 
      usingSpringWithDamping: 0.25, 
      initialSpringVelocity: 0.0, 
      options: [], 
      animations: {
        self.view.layer.position.x += 200.0
        self.view.layer.cornerRadius = 50.0
        self.view.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0)
    }, completion: nil)

chain.cancelAnimationChain({ self.myView.center = initialPosition //etc. etc. })

jiangleligejiang commented 5 years ago

二、源码探究

@objc
func EA_actionForLayer(_ layer: CALayer!, forKey key: String!) -> CAAction! {

    let result = EA_actionForLayer(layer, forKey: key)

    if let activeContext = EasyAnimation.activeAnimationContexts.last {
        if let _ = result as? NSNull {
            //检查属性值是否在所列举的属性集合中
            if vanillaLayerKeys.contains(key) ||
                (specializedLayerKeys[layer.classForCoder.description()] != nil && specializedLayerKeys[layer.classForCoder.description()]!.contains(key)) {

                    var currentKeyValue = layer.value(forKey: key)

                    //exceptions
                    if currentKeyValue == nil && key.hasSuffix("Color") {
                        currentKeyValue = UIColor.clear.cgColor
                    }

                    //found an animatable property - add the pending animation
                    if let currentKeyValue = currentKeyValue {
                        activeContext.pendingAnimations.append( //创建一个animation对象,并保存
                            PendingAnimation(layer: layer, keyPath: key, fromValue: currentKeyValue)
                        )
                    }
            }
        } else {
            activeContext.nrOfUIKitAnimations+=1
        }
    }

    return result
}

这里作者通过属性检查之后,创建一个PendingAnimation对象,并存入pendingAnimations队列中. 当我们调用animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)方法时,其实运行时系统会调用到下面对应的方法:

@objc
class func EA_animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?) {

    //create context
    let context = AnimationContext()
    context.duration = duration
    context.delay = delay
    context.options = options

    //push context
    EasyAnimation.activeAnimationContexts.append(context)

    //enable layer actions
    CATransaction.begin()
    CATransaction.setDisableActions(false)

    var completionBlock: CompletionBlock? = nil

    //animations
    if let completion = completion {
        //wrap a completion block
        completionBlock = CompletionBlock(context: context, completion: completion)
        EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: completionBlock!.wrapCompletion)
    } else {
        //simply schedule the animation
        EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: nil)
    }

    //pop context
    EasyAnimation.activeAnimationContexts.removeLast()

    //run pending animations
    for anim in context.pendingAnimations {
        //print("pending: \(anim.keyPath) from \(anim.fromValue) to \(anim.layer.value(forKeyPath: anim.keyPath))")
        anim.layer.add(EA_animation(anim, context: context), forKey: nil)
    }

    //try a timer now, than see about animation delegate
    if let completionBlock = completionBlock, context.nrOfUIKitAnimations == 0, context.pendingAnimations.count > 0 {
        Timer.scheduledTimer(timeInterval: context.duration, target: self, selector: #selector(UIView.EA_wrappedCompletionHandler(_:)), userInfo: completionBlock, repeats: false)
    }

    CATransaction.commit()
 }

这里比较值得注意的有两点:

  1. 下面调用的EA_animate方法其实是调用了UIView.animate的系统方法,因为在之前已经将两者互换了。
    //animations
    if let completion = completion {
        //wrap a completion block
        completionBlock = CompletionBlock(context: context, completion: completion)
        EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: completionBlock!.wrapCompletion)
    } else {
        //simply schedule the animation
        EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: nil)
    }
  2. 这里使用显式的方式执行pendingAnimations队列中的动画
    //run pending animations
    for anim in context.pendingAnimations {
        //print("pending: \(anim.keyPath) from \(anim.fromValue) to \(anim.layer.value(forKeyPath: anim.keyPath))")
        anim.layer.add(EA_animation(anim, context: context), forKey: nil)
    }

    其实上面的两点是串联起来的,当调用到EA_animate方法(实际上为UIView.animate),会触发到EA_actionForLayer方法(实际上为UIView.actionForLayer),然后获取到相关动画属性,并添加到pendingAnimations队列中,最后通过anim.layer.add的方式显式地进行相关动画。

jiangleligejiang commented 5 years ago

作者通过存储前后的EAAnimationFuture对象来实现一个双向队列,类似A ⇆ B ⇆ C,当A完成动画之后,会启动B去执行动画,同理C。

/* animation chain links */
var prevDelayedAnimation: EAAnimationFuture? {
    didSet {
        if let prev = prevDelayedAnimation {
            identifier = prev.identifier
        }
    }
}
var nextDelayedAnimation: EAAnimationFuture?

当我们调用animateAndChain时,会创建一个当前的EAAnimationFuture和下一个EAAnimationFuture对象,并返回。这样可以保证可以继续链式调用animate方法。

public class func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture {

    let currentAnimation = EAAnimationFuture()
    currentAnimation.duration = duration
    currentAnimation.delay = delay
    currentAnimation.options = options
    currentAnimation.animations = animations
    currentAnimation.completion = completion

    currentAnimation.nextDelayedAnimation = EAAnimationFuture()
    currentAnimation.nextDelayedAnimation!.prevDelayedAnimation = currentAnimation
    currentAnimation.run()

    EAAnimationFuture.animations.append(currentAnimation)

    return currentAnimation.nextDelayedAnimation!
}

调用后,会调用run方法开始当前currentAnimation的动画:

func run() {
    if debug {
        print("run animation #\(debugNumber)")
    }
    //TODO: Check if layer-only animations fire a proper completion block
    if let animations = animations {
        options.insert(.beginFromCurrentState)
        let animationDelay = DispatchTime.now() + Double(Int64( Double(NSEC_PER_SEC) * self.delay )) / Double(NSEC_PER_SEC)

        DispatchQueue.main.asyncAfter(deadline: animationDelay) {
            if self.springDamping > 0.0 {
                //spring animation
                UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: self.springDamping, initialSpringVelocity: self.springVelocity, options: self.options, animations: animations, completion: self.animationCompleted)
            } else {
                //basic animation
                UIView.animate(withDuration: self.duration, delay: 0, options: self.options, animations: animations, completion: self.animationCompleted)
            }
        }
    }
}

这里是通过DispatchQueue.main.asyncAfter方法来实现延时动画的,动画开始之后,通过自定义的animationCompleted方法来监听动画完成:

private func animationCompleted(_ finished: Bool) {

    //animation's own completion
    self.completion?(finished)

    //chain has been cancelled
    if let cancelCompletion = EAAnimationFuture.cancelCompletions[identifier] {
        if debug {
            print("run chain cancel completion")
        }
        cancelCompletion()
        detachFromChain()
        return
    }

    //check for .Repeat
    if finished && self.loopsChain {
        //find first animation in the chain and run it next
        var link = self
        while link.prevDelayedAnimation != nil {
            link = link.prevDelayedAnimation!
        }
        if debug {
            print("loop to \(link)")
        }
        link.run()
        return
    }

    //run next or destroy chain
    if self.nextDelayedAnimation?.animations != nil {
        self.nextDelayedAnimation?.run()
    } else {
        //last animation in the chain
        self.detachFromChain()
    }

}

这里可以分为三个点:

  1. 判断动画是否被取消了,若动画被取消了,则调用detachFromChain方法将动画移除

    private func detachFromChain() {
       self.nextDelayedAnimation = nil
       //从当前开始向队首递进,直到到达队首,在animations集合中移除掉该动画
       if let previous = self.prevDelayedAnimation {
           if debug {
               print("dettach \(self)")
           }
           previous.nextDelayedAnimation = nil
           previous.detachFromChain()
       } else {
           if let index = EAAnimationFuture.animations.index(of: self) {
               if debug {
                   print("cancel root animation #\(EAAnimationFuture.animations[index])")
               }
               EAAnimationFuture.animations.remove(at: index)
           }
       }
       self.prevDelayedAnimation = nil
    }
  2. 若动画被设定为repeat,则会找到队首的动画对象,并重复执行该系列动画效果

    //check for .Repeat
    if finished && self.loopsChain {
       //find first animation in the chain and run it next
       var link = self
       while link.prevDelayedAnimation != nil {
           link = link.prevDelayedAnimation!
       }
       if debug {
           print("loop to \(link)")
       }
       link.run()
       return
    }
  3. 若当前动画对象存在下一个动画对象,则执行下一个动画对象,否则说明达到队尾,进行相关移除操作

    //run next or destroy chain
    if self.nextDelayedAnimation?.animations != nil {
    self.nextDelayedAnimation?.run()
    } else {
    //last animation in the chain
    self.detachFromChain()
    }   
    • 如何实现取消动画

      作者在执行完animate方法之后会返回一个EAAnimationFuture对象,可通过该对象来取消动画

      
      private static var cancelCompletions: [String: ()->Void] = [:]

public func cancelAnimationChain(_ completion: (()->Void)? = nil) { EAAnimationFuture.cancelCompletions[identifier] = completion

    var link = self
    while link.nextDelayedAnimation != nil {
        link = link.nextDelayedAnimation!
    }

    link.detachFromChain()

    if debug {
        print("cancelled top animation: \(link)")
    }
}

如上所示,EAAnimationFuture中保存了一个`cancelCompletions`字典对象,在上一点中,可以看到每次动画执行完后,都会判断是否存在取消操作,若存在,则直接移除后续的相关操作,从而达到取消动画的效果。
jiangleligejiang commented 5 years ago

总结