迁移至 Swift 3.1 支持
FlightAnimator 提供了一种非常简单的基于块的动画定义语言,允许您动态地创建、配置、分组、序列化、缓存和重用属性动画。
与使用单个缓动曲线同时动画化多个属性的 CAAnimationGroups
和 UIViewAnimations
不同,FlightAnimator 允许为每个单独的属性动画进行配置和同步独特的缓动曲线。
在以下视频中的 FlightAnimator 项目演示 中
体验 FlightAnimator 的所有不同功能。
FlightAnimator 提供了一种非常灵活的语法,用于简化动画定义,可以从简单到复杂。遵循基于块的构建方法,您可以轻松定义一个动画组,以及它的属性动画。
在底层,构建的动画是带有多个自定义的 CAKeyframeAnimation
的 CAAnimationGroup
。一旦动画时间到来,FlightAnimator 将动态同步所有动画相对于当前 presentationLayer 的值的剩余进度,然后继续动画到最后的状态。
要真正了解 FlightAnimator 的强大功能,我们首先使用 CoreAnimation
定义一个动画,然后使用框架基于块的语法重新定义它。下面的动画使用了 CAAnimationGroup
将3个单独的 CABasicAnimations
(alpha、边界和位置)组合在一起。
let alphaAnimation = CABasicAnimation(keyPath: "position")
alphaAnimation.toValue = 0.0
alphaAnimation.fromValue = 1.0
alphaAnimation.fillMode = kCAFillModeForwards
alphaAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let boundsAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnimation.toValue = NSValue(CGRect : toBounds)
boundsAnimation.fromValue = NSValue(CGRect : view.layer.bounds)
boundsAnimation.fillMode = kCAFillModeForwards
boundsAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.toValue = NSValue(CGPoint : toPosition)
positionAnimation.fromValue = NSValue(CGPoint : view.layer.position)
positionAnimation.fillMode = kCAFillModeForwards
positionAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let progressAnimation = CABasicAnimation(keyPath: "animatableProgress")
progressAnimation.toValue = 1.0
progressAnimation.fromValue = 0
progressAnimation.fillMode = kCAFillModeForwards
progressAnimation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
let animationGroup = CAAnimationGroup()
animationGroup.duration = 0.5
animationGroup.removedOnCompletion = true
animationGroup.animations = [alphaAnimation, boundsAnimation, positionAnimation, progressAnimation]
view.layer.addAnimation(animationGroup, forKey: "PositionAnimationKey")
view.frame = toFrame
现在我们已经看到了上面的示例,让我们重新定义 FlightAnimator 的基于块的语法
view.animate { [unowned self] (animator) in
animator.alpha(toAlpha).duration(0.5).easing(.OutCubic)
animator.bounds(toBounds).duration(0.5).easing(.OutCubic)
animator.position(toPosition).duration(0.5).easing(.OutCubic)
animator.value(toProgress, forKeyPath : "animatableProgress").duration(0.5).easing(.OutCubic)
}
在 view 上调用 animate(:)
开始于 FAAnimationGroup
的创建过程。在创建的闭包内部,animator 构造、配置,并将自定义动画追加到新创建的父组中。通过调用一个 预定义属性设置器,定义每个单独的属性动画,或者为任何其他可动画属性调用 func value(:, forKeyPath:) -> PropertyAnimator
方法。
一旦启动属性动画,递归配置 PropertyAnimator
以添加持续时间、缓动和/或主要设计,从而创建最终的 FABasicAnimation
,并将其添加到父组中。
func duration(duration : CGFloat) -> PropertyAnimator
func easing(easing : FAEasing) -> PropertyAnimator
func primary(primary : Bool) -> PropertyAnimator
一旦函数调用退出闭包,FlightAnimator 将执行以下操作
FAAnimationGroup
添加到调用 view 的层中,FABasicAnimations
在 FlightAnimator 中将动画链式调用起来非常简单。
在 secondaryView 上创建的动画将在 primaryView 的动画开始时触发。
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
....
})
}
在 secondaryView 上创建的动画将在 primaryView 的动画完成时触发。
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnCompletion(onView: self.secondaryView, animator: { (animator) in
....
})
}
当主视图上的驱动动画达到相对的中途时,secondaryView 上创建的动画将被触发。
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnProgress(0.5, onView: self.secondaryView, animator: { (animator) in
....
})
}
当驱动动画达到主视图动画 fromValue 和 toValue 间的相对一半距离时,secondaryView 上创建的动画将被触发。
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnValueProgress(0.5, onView: self.secondaryView, animator: { (animator) in
....
})
}
有内置的支持在触发器内嵌套触发器以安排动画,并相对于父动画的范围附加多类型触发器。
primaryView.animate { [unowned self] (animator) in
....
animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
-> Relative to primaryView animation
animator.triggerOnCompletion(onView: self.tertiaryView, animator: { (animator) in
-> Relative to secondaryView animation
animator.triggerOnProgress(0.5, onView: self.quaternaryView, animator: { (animator) in
-> Relative to tertiaryView animation
})
})
animator.triggerOnValueProgress(0.5, onView: self.quinaryView, animator: { (animator) in
-> Relative to secondaryView animation
})
})
animator.triggerOnStart(onView: self.senaryView, animator: { (animator) in
-> Relative to primaryView animation
})
}
有时需要通过响应 CAAnimationDelegate 方法在动画开始或结束时执行一些逻辑。
view.animate { (animator) in
....
animator.setDidStartCallback({ (animator) in
// Animation Did Start
})
animator.setDidStopCallback({ (animator, complete) in
// Animation Did Stop
})
}
这些可以像动画触发器一样嵌套,并且可以通过动画器将动画器应用于动画创建闭包中定义的动画范围内的组。
FlightAnimator 允许在最初注册动画(即状态),使用唯一的动画密钥。一旦定义,可以随时在应用的流程中使用注册时使用的动画密钥手动触发。
当应用动画时,如果视图处于飞行中,它将自动与当前呈现层的值同步,并动画到最后目的地。
要注册一个动画,调用一个全局定义的方法,并在制作块中创建一个动画,就像以前示例中所定义的那样。以下示例显示了如何注册和缓存指定视图上的动画键。
struct AnimationKeys {
static let CenterStateFrameAnimation = "CenterStateFrameAnimation"
}
...
view.registerAnimation(forKey : AnimationKeys.CenterStateFrameAnimation) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
animator.position(newPositon).duration(0.5).easing(.OutCubic)
})
此动画仅进行缓存,直到手动触发才会执行。
要触发动画,请调用以下代码
view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation)
要应用最终值而不使视图动起来,重写默认的动画标志为假,并将所有最终值应用到相关视图的模型层。
view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation, animated : false)
由于框架的动态特性,可能需要进行一些调整以获得理想的动画。
FlightAnimator有几个选项,可以更精确地控制时间同步。
首先是一个简单的背景,框架基本上通过优先考虑最大剩余时间来同步时间,如果在飞行过程中被重新定向。
让我们看以下示例,在组动画上设置时间优先级为 .MaxTime,这是FlightAnimator的默认值。
func animateView(toFrame : CGRect) {
let newBounds = CGRectMake(0,0, toFrame.width, toFrame.height)
let newPosition = CGPointMake(toFrame.midX, toFrame.midY)
view.animate(.MaxTime) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
animator.position(newPositon).duration(0.5).easing(.InSine)
}
}
就像演示应用程序一样,此方法由不同的按钮调用,并采用触发该方法的按钮的帧值。让我们假设动画已经被触发,并且正在飞行中。当在飞行中另一个按钮被点击时,应用一个新的动画,位置发生变化,但边界保持不变。
框架内部将根据上次动画的当前进度确定进度,并从分组属性动画的持续时间数组中选择最长持续时间值。
假设边界没有变化,因此假设动画的持续时间为0.0。新的动画将根据进度与位置动画的持续时间同步,并自动根据 .MaxTime 时间优先级成为最长持续时间。
时间优先级还可以用于 triggerAtTimeProgress()
或 triggerAtValueProgress()
。现在让我们继续下一个主题,那就是主标志。
在组内具有越多的属性动画,调整时间应用方式的必要性越大。为此,可以选择4种时间优先级:
就像之前的示例中提到的,动画可能非常复杂,组内的属性动画越多,动画在时间上的颠簸可能性越大,尤其是在同步不同曲线和持续时间的4个以上的动画时。
为此,可以在单个属性动画上设置主标志,并将它们指定为主持续时间驱动器。默认情况下,如果没有属性动画设置为主,在同步期间,FlightAnimator将使用时间优先级设置从所有动画中找到进度同步后的相应值。
如果只需要定义进度的某些特定属性动画,并将其作为主要驱动因素,请将主要标志设置为 true,这将排除所有其他未标记为主要的动画,不考虑它们。
以下是一个示例,演示了一个简单的视图如何使用边界和位置从当前位置动画过渡到一个新帧。
view.animate(.MaxTime) { (animator) in
animator.bounds(newBounds).duration(0.5).easing(.OutCubic).primary(true)
animator.position(newPositon).duration(0.5).easing(.InSine).primary(true)
animator.alpha(0.0).duration(0.5).easing(.OutCubic)
animator.transform(newTransform).duration(0.5).easing(.InSine)
}
就是这样简单,现在当视图在动画过程中被重新定位时,只有边界和位置动画将被考虑作为定时同步的一部分。
当使用 UIPanGestureRecognizer 通过调整视图位置在其屏幕上移动视图时,例如,需要平滑地将视图动画化到最终位置,正好在用户释放手势时。这时 .SpringDecay 缓动就派上用场了。.SpringDecay 缓动会让视图轻松减速并进入位置,只需配置初始速度,它就会根据速度自行计算前往目的地的相对时间。
以下是处理交云并使用 .SpringDecay(velocity: velocity)
缓动执行动画的示例。
func respondToPanRecognizer(recognizer : UIPanGestureRecognizer) {
switch recognizer.state {
........
case .Ended:
let currentVelocity = recognizer.velocityInView(view)
view.animate { (animator) in
animator.bounds(finalBounds).duration(0.5).easing(.OutCubic)
animator.position(finalPositon).duration(0.5).easing(.SpringDecay(velocity: velocity))
}
default:
break
}
}
项目包含一个高度可配置的演示应用,允许进行实验以探索 FlightAnimator 支持的无尽配置产生的效果。
包含的演示功能
FlightAnimator 采用了 MIT 许可证发布。有关详情,请参阅 许可证。