实现一个简单的动画链式调用

最近在写一个新的自定义 Alert 的动画时,感觉这样的简单动画代码在写 App 的时候很容易用到。有时候有一些简单的连续动画,可能也就2,3 个动作,但是直接使用 UIKit 的 CoreAnimation 动画的话,就需要不停的在 Completion Block中连续调用,这样的代码看起来太丑了,但是为了小小的动画再引入一个三方的动画框架,无论是代价还是灵活性都不如自己去实现。接下来就是代码的实现。

我们实现的思路就是封装 UIView 的 Animation 方法,一个动画的动作需要两个参数,一个是动画类型,一个是用来修改属性的 Block。然后我们还需要一个参数来保存动画执行完毕后需要执行的下一个动画。

我们创建一个类型 PromiseAnimation, type表示当前动画的类型;handle 是动画执行的 block,在 normal类型动画当中,这个 handle 是修改属性的 block,而在 once 当中这是一个简单的回调,通常用来执行一个动画完成后需要执行的操作。thenAnimation 就是表示下一个需要执行的动画(不一定是动画,可能只是一个操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
final class PromiseAnimation {
typealias Handle = () -> Void
enum AnimationType {
case normal(duration: TimeInterval = 0.2, delay: TimeInterval = 0, options: UIView.AnimationOptions = [])
case once
case delay(_ duratoin: DispatchTimeInterval)
}

private let type: AnimationType
private let handle: Handle
private var thenAnimation: PromiseAnimation?

@discardableResult init(_ type: AnimationType = .once, handle: @escaping Handle) {
self.type = type
self.handle = handle
run()
}

private init(_ type: AnimationType, _ handle: @escaping Handle) {
self.type = type
self.handle = handle
}

private func run() {
switch type {
case let .normal(duration: duration, delay: delay, options: options):
UIView.animate(withDuration: duration, delay: delay, options: options, animations: handle, completion: runNext(_:))
case .once:
handle()
runNext(true)
case let .delay(duratoin: duration):
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: { self.runNext(true) })
}
}

private func runNext(_ success: Bool) {
thenAnimation?.run()
thenAnimation = nil
}
}

这里有两个不一样的 init 方法,是因为第一个动画需要自动执行,而后续的 Animation 则需要手动调用。公开初始化一个 PromiseAnimation时会直接调用 run() 来执行动画,而在 then 方法中生成的 Animation 则需要通过 runNext() 来择机调用,接下来就是链式调用的实现,这里模仿 PromiseKit 使用一个 then 来连接下一个动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension PromiseAnimation {
/// Perform this anmation when last animation finished.
@discardableResult public func then(_ type: AnimationType = .normal(), handle: @escaping Handle) -> PromiseAnimation {
let animation = PromiseAnimation(type, handle)
thenAnimation = animation
return animation
}

/// Immediately perform.
@discardableResult public func once(handle: @escaping Handle) -> PromiseAnimation {
then(.once, handle: handle)
}

/// Delay
@discardableResult public func delay(_ duration: DispatchTimeInterval) -> PromiseAnimation {
then(.delay(duration), handle: {})
}
}

这里可以看出我们在 then 中需要传入下一个动画所需要的所有参数,然后生成一个新的 Animation 并返回,在其中需要把新生成的 Animation 赋值给当前的 thenAnimation 用来在当前的动画调用完毕以后去激活 thenAnimation,这是我们可以实现链式调用的核心,至此整个实现就已经完成了,我们来看一下实际的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func show() {
let box = UIView()

// ...

PromiseAnimation(.normal(duration: 0.2)) {
box.backgroundColor = .red
box.center = .init(x: 100, y: 100)
}.then(.normal(duration: 1)) {
box.alpha = 0.2
box.center = .init(x: 200, y: 200)
}.delay(.seconds(2))
.once {
print("Animation end.")
}
}

简洁的链式调用让我们可以快速的理解动画的行为,也能减少调用深度,让代码看起来更整洁。