共计 7171 个字符,预计需要花费 18 分钟才能阅读完成。
本文作者:有恒
一、背景
日常开发过程中,常常须要对视图做动画,如果须要对一个 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 进行串行治理,不再赘述。
三、总结
本文只是对动画回调嵌套问题的轻量化解决方案,让组合动画的代码构造更加清晰,不便开发和后续迭代批改。实现计划还有许多能够改良的中央,欢送参考斧正。
四、参考资料
- 图源:https://unsplash.com/photos/PDxp-AItBMA
- [Declarative animation]https://www.swiftbysundell.com/articles/building-a-declarativ…
- [CAAnimation] Apple Inc. Core Animation Programming Guide. [About Core Animation](About Core Animation)
- [CAAnimation] 王巍. iOS 动画高级技巧 [M]. 北京:人民邮电出版社,2015.
- [CAAnimation]https://developer.apple.com/documentation/quartzcore/caanimat…
- [CAAnimation]https://github.com/pro648/tips/blob/master/sources/CAAnimation%EF%BC%9A%E5%B1%9E%E6%80%A7%E5%8A%A8%E7%94%BBCABasicAnimation%E3%80%81CAKeyframeAnimation%E4%BB%A5%E5%8F%8A%E8%BF%87%E6%B8%A1%E5%8A%A8%E7%94%BB%E3%80%81%E5%8A%A8%E7%94%BB%E7%BB%84.md
- [UIViewPropertyAnimator]https://developer.apple.com/documentation/uikit/uiviewpropert…
- [应用 UIViewPropertyAnimator 做动画]https://swift.gg/2017/04/20/quick-guide-animations-with-uivie…
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe (at) corp.netease.com!