Open jiangleligejiang opened 5 years ago
如何使用系统方法UIView.animate
就可以实现自己的操作
作者其实是通过
category+swizzle
的方式来进行替换系统方法extension UIView {
// MARK: UIView animation & action methods
fileprivate static func replaceAnimationMethods() { //replace actionForLayer... if let origMethod = class_getInstanceMethod(self, #selector(UIView.action(for:forKey:))), let eaMethod = class_getInstanceMethod(self, #selector(UIView.EAactionForLayer(:forKey:))) { method_exchangeImplementations(origMethod, eaMethod) }
//replace animateWithDuration...
if
let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:))),
let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:))) {
method_exchangeImplementations(origMethod, eaMethod)
}
if
let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:completion:))),
let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:completion:))) {
method_exchangeImplementations(origMethod, eaMethod)
}
if
let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:options:animations:completion:))),
let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:options:animations:completion:))) {
method_exchangeImplementations(origMethod, eaMethod)
}
if
let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:))),
let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:))) {
method_exchangeImplementations(origMethod, eaMethod)
}
} }
**这里有个比较值得关注的问题:如何获取到外部所修改的属性,然后再做响应的自定义处理?**
举个简单的例子,如下:
```swift
UIView.animate(duration: 2.0, animations: {
self.view.layer.position.x = 200.0
})
像duration
这样的属性我们是可以获取到的,但对于position.x
这样的属性值我们要怎么获取呢?
其实,当我们对layer的属性进行修改时,layer 通过向它的 delegate 发送
actionForLayer:forKey:
消息来询问提供一个对应属性变化的 action。因此,作者swizzle
了actionForLayer:forKey:
方法,以此获取到所改变的动画属性。
@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()
}
这里比较值得注意的有两点:
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)
}
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
的方式显式地进行相关动画。
如何在iOS8上支持Spring Layer Animations
回到上面的anim.layer.add(EA_animation(anim, context: context), forKey: nil)
,此处调用了EA_animation
方法创建了一个CAAnimation
对象:
private class func EA_animation(_ pending: PendingAnimation, context: AnimationContext) -> CAAnimation {
let anim: CAAnimation
if (context.springDamping > 0.0) {
//create a layer spring animation
if #available(iOS 9, *) { // iOS9!
anim = CASpringAnimation(keyPath: pending.keyPath)
if let anim = anim as? CASpringAnimation {
anim.fromValue = pending.fromValue
anim.toValue = pending.layer.value(forKey: pending.keyPath)
let epsilon = 0.001
anim.damping = CGFloat(-2.0 * log(epsilon) / context.duration)
anim.stiffness = CGFloat(pow(anim.damping, 2)) / CGFloat(pow(context.springDamping * 2, 2))
anim.mass = 1.0
anim.initialVelocity = 0.0
}
} else {
anim = RBBSpringAnimation(keyPath: pending.keyPath)
if let anim = anim as? RBBSpringAnimation {
anim.from = pending.fromValue
anim.to = pending.layer.value(forKey: pending.keyPath)
//TODO: refine the spring animation setup
//lotta magic numbers to mimic UIKit springs
let epsilon = 0.001
anim.damping = -2.0 * log(epsilon) / context.duration
anim.stiffness = Double(pow(anim.damping, 2)) / Double(pow(context.springDamping * 2, 2))
anim.mass = 1.0
anim.velocity = 0.0
}
}
} else {
//create property animation
anim = CABasicAnimation(keyPath: pending.keyPath)
(anim as! CABasicAnimation).fromValue = pending.fromValue
(anim as! CABasicAnimation).toValue = pending.layer.value(forKey: pending.keyPath)
}
anim.duration = context.duration
if context.delay > 0.0 {
anim.beginTime = context.currentTime + context.delay
anim.fillMode = CAMediaTimingFillMode.backwards
}
//options
.......
return anim
}
如上所示,对应iOS9以下的系统,使用了RBBSpringAnimation
来代替系统CASpringAnimation
,因为CASpringAnimation
是只支持iOS9+的。
作者通过存储前后的
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()
}
}
这里可以分为三个点:
判断动画是否被取消了,若动画被取消了,则调用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
}
若动画被设定为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
}
若当前动画对象存在下一个动画对象,则执行下一个动画对象,否则说明达到队尾,进行相关移除操作
//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`字典对象,在上一点中,可以看到每次动画执行完后,都会判断是否存在取消操作,若存在,则直接移除后续的相关操作,从而达到取消动画的效果。
若需要对系统相关API进行改进,可以考虑使用category+swizzle
的方式,这么做可以保证外部无隙接入,且需要移除的话,去除相关swizzle
操作即可。
理解了动画中View-Layer
之间的协作关系,其实当我们使用UIView.animate
动画,本质上系统内部都会调用到actionForKey
方法去获取到相关动画属性,并创建对应的CAAnimation
动画对象,最后添加到layer
中,具体可以看这里。
掌握了使用prev+cur+next
的方式创建双向队列,从而实现链式调用操作。
一、EasyAnimation简介
UIView.animate
方法便捷地修改动画属性EasyAnimation (after) UIView.animate(duration: 2.0, animations: { self.view.layer.position.x = 200.0 })
chain.cancelAnimationChain({ self.myView.center = initialPosition //etc. etc. })