本文作者:有恒
一、背景
日常开发过程中,经常需求对视图做动画,假设需求对一个 view 进行动画操作:3s 淡入,完毕后,1s 扩大,很容易写出这样的代码:
UIView.animate(withDuration: 3, animations: {
view.alpha = 1
}, completion: { _ in
UIView.animate(withDuration: 1) {
view.frame.size = CGSize(width: 200, height: 200)
}
})
如果,是更多串行的动画需求完成呢?
UIView.animate(withDuration: 3, animations: {
......
}, completion: { _ in
UIView.animate(withDuration: 3) {
......
}, completion: { _ in
UIView.animate(withDuration: 3) {
......
}, completion: { _ in
......
}
}
})
这样的回调阴间代码,很难保护也不高雅。
业界也有一些现成的动画库,比较闻名的有:
- Spring: 轻量级的、根据 Swift 完成的动画库,它供给了多种绷簧作用动画作用。缺陷是功用相对较少,不能满意所有的动画需求。
- Hero:一个高度可定制化的 iOS 动画库,它支撑多种动画作用,如过渡动画、视图转场等。缺陷是对于杂乱的动画作用或许需求编写很多的代码。
- TweenKit:一个轻量级的、根据 Swift 完成的动画库,它供给了多种动画作用,如突变作用、旋转作用等。TweenKit 的长处是易于运用,对于入门级的开发者很友爱,但缺陷是功用相对较少,不能满意所有的动画需求。
以上动画库各有长处和缺陷,总的来说都有书写相对杂乱不够高雅的缺陷,那有没有便利开发和保护的代码格局?
动画串行履行:
view.at.animate(
.fadeIn(duration: 3.0),
.scale(toValue: 1.2, duration: 0.5)
)
动画并行履行:
view.at.animate(parallelism:
.fadeIn(duration: 3.0),
.scale(toValue: 1.2, duration: 1)
)
如果是多个视图组合动画串行履行呢?
AT.animate (
view1.at.animate(parallelism:
.fadeIn(duration: 3.0),
.scale(toValue: 1.2, duration: 1)
)
view2.at.animate(parallelism:
.fadeIn(duration: 3.0),
.scale(toValue: 1.2, duration: 1)
)
)
二、完成方案
Animator
动画履行器
public protocol Animator {
associatedtype T
mutating func start(with view : UIView)
func pause()
func resume()
func stop()
}
封装 UIViewPropertyAnimator,CAKeyframeAnimator,CABasicAnimator,遵从 Animator 协议,完成不同类型的动画履行器。
Animation
Animation 供给动画履行参数:
为不同的 Animator 制定不同的 Animation 协议:
public protocol AnimationBaseProtocol {
var duration : TimeInterval { get }
}
protocol CAAnimationProtocol : AnimationBaseProtocol {
var repeatCount : Float { get }
var isRemovedOnCompletion : Bool { get }
var keyPath : String? { get }
var animationkey : String? { get }
}
protocol ProPertyAnimationProtocol : AnimationBaseProtocol {
var curve : UIView.AnimationCurve { get }
var fireAfterDelay : TimeInterval { get }
var closure : (UIView) -> Void { get }
}
protocol CABaseAnimationProtocol: CAAnimationProtocol {
var fromValue: Any { get }
var toValue : Any { get }
}
protocol CAKeyFrameAnimationProtocol: CAAnimationProtocol {
var keyTimes: [NSNumber]? { get }
var timingFunction: CAMediaTimingFunction? { get }
var valuesClosure: ((UIView) -> [Any]?) { get }
}
需求注意的是,动画履行器支撑多种完成,用到了范型,动画履行器作为返回值,运用时需求对其进行类型擦除。
类型擦除
类型擦除的作用是擦除范型的具体信息,以便在运行时运用:
界说一个范型类:
class Stack<T> {
var items = [T]()
func push(item: T) {
items.append(item)
}
func pop() -> T? {
if items.isEmpty {
return nil
} else {
return items.removeLast()
}
}
}
如果这样运用:
// 实例化一个 Stack<String> 目标
let stackOfString = Stack<String>()
stackOfString.push(item: "hello")
stackOfString.push(item: "world")
// 实例化一个 Stack<Int> 目标
let stackOfInt = Stack<Int>()
stackOfInt.push(item: 1)
stackOfInt.push(item: 2)
let stackArray: [Stack] = [stackOfString, stackOfInt]
会有一个过错:
由于这是两种类型。
怎么进行擦除?
class AnyStack {
private let pushImpl: (_ item: Any) -> Void
private let popImpl: () -> Any?
init<T>(_ stack: Stack<T>) {
pushImpl = { item in
if let item = item as? T {
stack.push(item: item)
}
}
popImpl = {
return stack.pop()
}
}
func push(item: Any) {
pushImpl(item)
}
func pop() -> Any? {
return popImpl()
}
}
这样履行下面代码就能够正常编译运用:
let stackArray: [AnyStack] = [AnyStack(stackOfString), AnyStack(stackOfInt)]
回到 Animator 的设计,同样的原理,这样就解决了形参类型不一致的问题。
具体完成
extension Animator {
public static func fadeIn(duration: TimeInterval = 0.25, curve:UIView.AnimationCurve = .linear , fireAfterDelay: TimeInterval = 0.0, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
let propertyAnimation = PropertyAnimation()
propertyAnimation.duration = duration
propertyAnimation.curve = curve
propertyAnimation.fireAfterDelay = fireAfterDelay
propertyAnimation.closure = { $0.alpha = 1}
return Self.creatAnimator(with: propertyAnimation,completion: completion)
}
public static func scale(valus: [NSNumber], keyTimes: [NSNumber], repeatCount: Float = 1.0, duration: TimeInterval = 0.3, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
let animation = CAFrameKeyAnimation()
animation.keyTimes = keyTimes
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.valuesClosure = {_ in valus}
animation.repeatCount = repeatCount
animation.isRemovedOnCompletion = true
animation.fillMode = .removed
animation.keyPath = "transform.scale"
animation.animationkey = "com.moyi.animation.scale.times"
animation.duration = duration
return AnyAnimator.init(CAKeyFrameAnimator(animation: animation,completion: completion))
}
/// 自界说Animation
public static func creatAnimator(with propertyAnimation : PropertyAnimation, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
return AnyAnimator.init(ViewPropertyAnimator(animation:propertyAnimation,completion:completion))
}
}
CAAnimation 是 Core Animation 结构中负责动画作用的类,它界说了一系列动画作用相关的特点和办法。能够通过创建 CAAnimation 的子类,如 CABasicAnimation、CAKeyframeAnimation、CAAnimationGroup 等来完成不同类型的动画作用。
其中,keypath 是 CAAnimation 的一个重要概念,用于指定动画作用所作用的特点。keypath 的值一般为字符串类型,在指定特点时需求运用 KVC(键值编码)来进行拜访。
更多关于 CAAnimation 的内容能够参阅引用中相关链接,不是本文重点不再展开。
AnimationToken
AnimationToken 是视图和动画履行器的封装,用于视图的动画处理。
然后对 UIView 增加串行、并行的扩展办法:
extension EntityWrapper where This: UIView {
internal func performAnimations<T>(_ animators: [AnyAnimator<T>] , completionHandlers: [(() -> Void)]) {
guard !animators.isEmpty else {
completionHandlers.forEach({ handler in
handler()
})
return
}
var leftAnimations = animators
var anyAnimator = leftAnimations.removeFirst()
anyAnimator.start(with: this)
anyAnimator.append {
self.performAnimations(leftAnimations, completionHandlers: completionHandlers)
}
}
internal func performAnimationsParallelism<T>(_ animators: [AnyAnimator<T>], completionHandlers: [(() -> Void)]) {
guard !animators.isEmpty else {
completionHandlers.forEach({ handler in
handler()
})
return
}
let animationCount = animators.count
var completionCount = 0
let animationCompletionHandler = {
completionCount += 1
if completionCount == animationCount {
completionHandlers.forEach({ handler in
handler()
})
}
}
for var animator in animators {
animator.start(with: this)
animator.append {
animationCompletionHandler()
}
}
}
}
completionHandlers 是动画使命的完毕的回调逻辑,类似 UIView 类办法 animate 的 completion 回调,这样就有了动画完毕的回调能力。
给 UIView 增加扩展,完成 view.at.animate () 办法:
extension EntityWrapper where This: UIView {
@discardableResult private func animate<T>(_ animators: [AnyAnimator<T>]) -> AnimationToken<T> {
return AnimationToken(
view: this,
animators: animators,
mode: .inSequence
)
}
@discardableResult public func animate<T>(_ animators: AnyAnimator<T>...) -> AnimationToken<T> {
return animate(animators)
}
@discardableResult private func animate<T>(parallelism animators: [AnyAnimator<T>]) -> AnimationToken<T> {
return AnimationToken(
view: this,
animators: animators,
mode: .parallelism
)
}
@discardableResult public func animate<T>(parallelism animators: AnyAnimator<T>...) -> AnimationToken<T> {
return animate(parallelism: animators)
}
}
AT.animate () 对 AnimationToken 进行串行办理,不再赘述。
三、总结
本文仅仅对动画回调嵌套问题的轻量化解决方案,让组合动画的代码结构愈加明晰,便利开发和后续迭代修改。完成方案还有许多能够改进的地方,欢迎参阅指正。
四、参阅资料
- 图源:unsplash.com/photos/PDxp…
- [Declarative animation]www.swiftbysundell.com/articles/bu…
- [CAAnimation] Apple Inc. Core Animation Programming Guide. [About Core Animation](About Core Animation)
- [CAAnimation] 王巍. iOS 动画高档技巧 [M]. 北京:人民邮电出版社,2015.
- [CAAnimation]developer.apple.com/documentati…
- [CAAnimation]github.com/pro648/tips…
- [UIViewPropertyAnimator]developer.apple.com/documentati…
- [运用 UIViewPropertyAnimator 做动画]swift.gg/2017/04/20/…
本文发布自网易云音乐技能团队,文章未经授权禁止任何形式的转载。咱们终年接收各类技能岗位,如果你准备换作业,又恰好喜欢云音乐,那就加入咱们 grp.music-fe (at) corp.netease.com!