iOS 复杂的动画协调如 Android Animator(Set)
iOS complex animation coordination like Android Animator(Set)
我使用 Animator 类 在我的 Android 应用程序中制作了一个相当复杂的动画。我想将此动画移植到 iOS。最好它有点像 Android Animator。我环顾四周,似乎没有什么是我想要的。我得到的最接近的是 CAAnimation。但不幸的是,如果将他们放在一个组中,所有子代表都会被忽略。
让我从我在 Android 上制作的动画开始。我正在为三个视图组制作动画(其中包含一个 ImageView 和一个 TextView)。每个按钮我有一个动画,将视图向左平移,同时将 alpha 设置为 0。在该动画之后,还有另一个动画将相同的视图从右侧平移到原始位置,并将 alpha 设置回 1。除了平移和 alpha 动画之外,还有一个视图还具有缩放动画。所有视图都使用不同的计时功能(缓动)。输入动画和输出动画是不同的,一个视图具有不同的缩放计时功能,而 alpha 和平移动画使用相同的功能。第一个动画结束后,我将设置值以准备第二个动画。缩放动画的持续时间也比平移和 alpha 动画短。我将单个动画(翻译和 alpha)放在 AnimatorSet(基本上是动画组)中。这个 AnimatorSet 被放在另一个 AnimatorSet 中以 运行 彼此之后的动画(第一个动画然后比在)。并且这个 AnimatorSet 被放在另一个 AnimatorSet 中,它 运行 同时为所有 3 个按钮设置动画。
抱歉解释得太长了。但是这样你就会明白我是如何尝试将它移植到 iOS 的。这个对于 UIView.animate() 来说太复杂了。如果放入 CAAnimationGroup,CAAnimation 会覆盖委托。据我所知,ViewPropertyAnimator 不允许自定义计时功能,并且无法协调多个动画。
有人知道我可以用它做什么吗?我也很喜欢自定义实现,它会在每个动画滴答时给我一个回调,这样我就可以相应地更新视图。
编辑
Android动画代码:
fun setState(newState: State) {
if(state == newState) {
return
}
processing = false
val prevState = state
state = newState
val reversed = newState.ordinal < prevState.ordinal
val animators = ArrayList<Animator>()
animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
displayMiddleButtonState()
}))
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
displayLeftButtonState()
}))
}
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(
if(newState == State.TAKE_PICTURE) rightButton else null,
if(newState == State.CROP_PICTURE) rightButton else null,
rightButton.imageView.width.toFloat(),
reversed,
halfAnimationDone = {
displayRightButtonState(inAnimation = true)
}))
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
animatorSet.start()
}
fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animators = ArrayList<Animator>()
if(animateInView != null) {
val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
if(animateOutView == null) {
animateInAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
halfAnimationDone()
}
})
}
animators.add(animateInAnimator)
}
if(animateOutView != null) {
val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
animators.add(animateOutAnimator)
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
return animatorSet
}
private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val fadeDuration = translateDuration
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, fadeAnimator)
return animateSet
}
fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
val animatorSet = AnimatorSet()
animatorSet.playTogether(animateInAnimator, animateOutAnimator)
return animatorSet
}
private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val scaleDuration = 100L
val fadeDuration = translateDuration
val maxTranslationXValue = middleButtonImageView.width.toFloat()
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val scaleValues =
if(animateIn) floatArrayOf(0.8f, 1f)
else floatArrayOf(1f, 0.8f)
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
scaleXAnimator.duration = scaleDuration
scaleYAnimator.duration = scaleDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
}
if(animateIn) {
val scaleStartDelay = translateDuration - scaleDuration
val scaleStartValue = scaleValues[0]
middleButtonImageView.scaleX = scaleStartValue
middleButtonImageView.scaleY = scaleStartValue
scaleXAnimator.startDelay = scaleStartDelay
scaleYAnimator.startDelay = scaleStartDelay
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)
return animateSet
}
编辑 2
这是动画在 Android 上的效果视频:
这是我认为您正在寻找的动画的开始。如果您不喜欢幻灯片的时间安排,则可以将 UIView.animate
换成 .curveEaseInOut
换成 CAKeyframeAnimation
,这样您可以更精细地控制每一帧。您需要为每个要设置动画的视图设置一个 CAKeyFrameAnimation
。
这是一个 playground,您可以将它复制并粘贴到一个空的 playground 中以查看它的运行情况。
import UIKit
import Foundation
import PlaygroundSupport
class ViewController: UIViewController {
let bottomBar = UIView()
let orangeButton = UIButton(frame: CGRect(x: 0, y: 10, width: 75, height: 75))
let yellow = UIView(frame: CGRect(x: 20, y: 20, width: 35, height: 35))
let magenta = UIView(frame: CGRect(x: 80, y: 30, width: 15, height: 15))
let cyan = UIView(frame: CGRect(x: 50, y: 20, width: 35, height: 35))
let brown = UIView(frame: CGRect(x: 150, y: 30, width:
15, height: 15))
let leftBox = UIView(frame: CGRect(x: 15, y: 10, width: 125, height: 75))
func setup() {
let reset = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
reset.backgroundColor = .white
reset.addTarget(self, action: #selector(resetAnimation), for: .touchUpInside)
self.view.addSubview(reset)
bottomBar.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
bottomBar.backgroundColor = .purple
self.view.addSubview(bottomBar)
orangeButton.backgroundColor = .orange
orangeButton.center.x = bottomBar.frame.size.width / 2
orangeButton.addTarget(self, action: #selector(orangeTapped(sender:)), for: .touchUpInside)
orangeButton.clipsToBounds = true
bottomBar.addSubview(orangeButton)
yellow.backgroundColor = .yellow
orangeButton.addSubview(yellow)
magenta.backgroundColor = .magenta
magenta.alpha = 0
orangeButton.addSubview(magenta)
// Left box is an invisible bounding box to get the effect that the view appeared from nowhere
// Clips to bounds so you cannot see the view when it has not been animated
// Try setting to false
leftBox.clipsToBounds = true
bottomBar.addSubview(leftBox)
cyan.backgroundColor = .cyan
leftBox.addSubview(cyan)
brown.backgroundColor = .brown
brown.alpha = 0
leftBox.addSubview(brown)
}
@objc func orangeTapped(sender: UIButton) {
// Perform animation
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
self.yellow.frame = CGRect(x: -20, y: 30, width: 15, height: 15)
self.yellow.alpha = 0
self.magenta.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
self.magenta.alpha = 1
self.cyan.frame = CGRect(x: -150, y: 30, width: 15, height: 15)
self.cyan.alpha = 0
self.brown.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
self.brown.alpha = 1
}, completion: nil)
}
@objc func resetAnimation() {
// Reset the animation back to the start
yellow.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
yellow.alpha = 1
magenta.frame = CGRect(x: 80, y: 30, width: 15, height: 15)
magenta.alpha = 0
cyan.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
cyan.alpha = 1
brown.frame = CGRect(x: 150, y: 30, width: 15, height: 15)
brown.alpha = 0
}
}
let viewController = ViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
viewController.view.backgroundColor = .blue
viewController.setup()
PlaygroundPage.current.liveView = viewController
所以我一直在使用 CADisplayLink
研究我自己的解决方案。文档是这样描述的 CADisplayLink
:
CADisplayLink is a timer object that allows your application to
synchronize its drawing to the refresh rate of the display.
它基本上在执行绘图代码时提供回调(因此您可以 运行 您的动画流畅)。
我不打算解释这个答案中的所有内容,因为它会有很多代码,而且大部分应该都很清楚。如果有什么不明白或者你有问题可以在这个答案下面评论。
此解决方案为动画提供了完全的自由,并提供了协调它们的能力。我看了很多 Android 上的 Animator
class 并想要类似的语法,以便我们可以轻松地将动画从 Android 移植到 iOS 或其他方式大约。我已经对其进行了几天的测试,并且也消除了一些怪癖。话不多说,让我们看一些代码吧!
这是 Animator
class,这是动画的基础结构 classes:
class Animator {
internal var displayLink: CADisplayLink? = nil
internal var startTime: Double = 0.0
var hasStarted: Bool = false
var hasStartedAnimating: Bool = false
var hasFinished: Bool = false
var isManaged: Bool = false
var isCancelled: Bool = false
var onAnimationStart: () -> Void = {}
var onAnimationEnd: () -> Void = {}
var onAnimationUpdate: () -> Void = {}
var onAnimationCancelled: () -> Void = {}
public func start() {
hasStarted = true
startTime = CACurrentMediaTime()
if(!isManaged) {
startDisplayLink()
}
}
internal func startDisplayLink() {
stopDisplayLink() // make sure to stop a previous running display link
displayLink = CADisplayLink(target: self, selector: #selector(animationTick))
displayLink?.add(to: .main, forMode: .commonModes)
}
internal func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
@objc internal func animationTick() {
}
public func cancel() {
isCancelled = true
onAnimationCancelled()
if(!isManaged) {
animationTick()
}
}
}
它包含所有重要信息,如启动 CADisplayLink
、提供停止 CADisplayLink
的能力(当动画完成时)、指示状态的布尔值和一些回调。您还会注意到 isManaged 布尔值。此布尔值表示 Animator
由一个组控制。如果是,该组将提供动画滴答声,并且此 class 不应启动 CADisplayLink
。
接下来是 ValueAnimator
:
class ValueAnimator : Animator {
public internal(set) var progress: Double = 0.0
public internal(set) var interpolatedProgress: Double = 0.0
var duration: Double = 0.3
var delay: Double = 0
var interpolator: Interpolator = EasingInterpolator(ease: .LINEAR)
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed: Double = (isCancelled) ? self.duration : CACurrentMediaTime() - startTime - delay
if(elapsed < 0) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
if(duration <= 0) {
progress = 1.0
} else {
progress = min(elapsed / duration, 1.0)
}
interpolatedProgress = interpolator.interpolate(elapsedTimeRate: progress)
updateAnimationValues()
onAnimationUpdate()
if(elapsed >= duration) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
internal func updateAnimationValues() {
}
}
这个class是所有价值动画师的基础class。但如果您想自己进行计算,它也可以用于您自己制作动画。您可能会注意到此处的 Interpolator
和 interpolatedProgress
。 Interpolator
class 稍后会显示。 class 提供了动画的缓动。这就是 interpolatedProgress
的用武之地。progress
只是从 0.0 到 1.0 的线性进展,但 interpolatedProgress
可能具有不同的缓动值。例如,当 progress
的值为 0.2 时,interpolatedProgress
可能已经有 0.4,具体取决于您将使用的缓动。还要确保使用 interpolatedProgress
来计算正确的值。 ValueAnimator
的第一个子 class 示例如下。
下面是 CGFloatValueAnimator
,顾名思义,它对 CGFloat 值进行动画处理:
class CGFloatValueAnimator : ValueAnimator {
private let startValue: CGFloat
private let endValue: CGFloat
public private(set) var animatedValue: CGFloat
init(startValue: CGFloat, endValue: CGFloat) {
self.startValue = startValue
self.endValue = endValue
self.animatedValue = startValue
}
override func updateAnimationValues() {
animatedValue = startValue + CGFloat(Double(endValue - startValue) * interpolatedProgress)
}
}
这是一个如何子 class ValueAnimator
的例子,如果你需要其他的,比如双精度数或整数,你可以做更多这样的例子。您只需提供开始值和结束值,Animator
根据 interpolatedProgress
计算当前的 animatedValue
是什么。您可以使用此 animatedValue
来更新您的观点。我会在最后展示一个例子。
因为我已经多次提到 Interpolator
,所以我们现在继续Interpolator
:
protocol Interpolator {
func interpolate(elapsedTimeRate: Double) -> Double
}
这只是一个您可以自己实现的协议。我将向您展示我自己使用的 EasingInterpolator
class 的一部分。如果有人需要,我可以提供更多。
class EasingInterpolator : Interpolator {
private let ease: Ease
init(ease: Ease) {
self.ease = ease
}
func interpolate(elapsedTimeRate: Double) -> Double {
switch (ease) {
case Ease.LINEAR:
return elapsedTimeRate
case Ease.SINE_IN:
return (1.0 - cos(elapsedTimeRate * Double.pi / 2.0))
case Ease.SINE_OUT:
return sin(elapsedTimeRate * Double.pi / 2.0)
case Ease.SINE_IN_OUT:
return (-0.5 * (cos(Double.pi * elapsedTimeRate) - 1.0))
case Ease.CIRC_IN:
return -(sqrt(1.0 - elapsedTimeRate * elapsedTimeRate) - 1.0)
case Ease.CIRC_OUT:
let newElapsedTimeRate = elapsedTimeRate - 1
return sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate)
case Ease.CIRC_IN_OUT:
var newElapsedTimeRate = elapsedTimeRate * 2.0
if (newElapsedTimeRate < 1.0) {
return (-0.5 * (sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate) - 1.0))
}
newElapsedTimeRate -= 2.0
return (0.5 * (sqrt(1 - newElapsedTimeRate * newElapsedTimeRate) + 1.0))
default:
return elapsedTimeRate
}
}
}
这些只是特定缓动计算的几个例子。我实际上移植了位于此处的 Android 的所有缓动:https://github.com/MasayukiSuda/EasingInterpolator.
在展示示例之前,我还有一个 class 要展示。哪个 class 允许动画师分组:
class AnimatorSet : Animator {
private var animators: [Animator] = []
var delay: Double = 0
var playSequential: Bool = false
override func start() {
super.start()
}
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed = CACurrentMediaTime() - startTime - delay
if(elapsed < 0 && !isCancelled) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
var finishedNumber = 0
for animator in animators {
if(!animator.hasStarted) {
animator.start()
}
animator.animationTick()
if(animator.hasFinished) {
finishedNumber += 1
} else {
if(playSequential) {
break
}
}
}
if(finishedNumber >= animators.count) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
public func addAnimator(_ animator: Animator) {
animator.isManaged = true
animators.append(animator)
}
public func addAnimators(_ animators: [Animator]) {
for animator in animators {
animator.isManaged = true
self.animators.append(animator)
}
}
override func cancel() {
for animator in animators {
animator.cancel()
}
super.cancel()
}
}
如您所见,这里是我设置 isManaged
布尔值的地方。您可以将您制作的多个动画师放入此 class 中以协调它们。因为这个 class 也扩展了 Animator
你也可以放入另一个 AnimatorSet
或多个。默认情况下它 运行 同时播放所有动画,但是如果 playSequential
设置为 true,它将 运行 所有动画按顺序播放。
演示时间:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let animView = UIView()
animView.backgroundColor = UIColor.yellow
self.view.addSubview(animView)
animView.snp.makeConstraints { maker in
maker.width.height.equalTo(100)
maker.center.equalTo(self.view)
}
let translateAnimator = CGFloatValueAnimator(startValue: 0, endValue: 100)
translateAnimator.delay = 1.0
translateAnimator.duration = 1.0
translateAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
translateAnimator.onAnimationStart = {
animView.backgroundColor = UIColor.blue
}
translateAnimator.onAnimationEnd = {
animView.backgroundColor = UIColor.green
}
translateAnimator.onAnimationUpdate = {
animView.transform.tx = translateAnimator.animatedValue
}
let alphaAnimator = CGFloatValueAnimator(startValue: animView.alpha, endValue: 0)
alphaAnimator.delay = 1.0
alphaAnimator.duration = 1.0
alphaAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
alphaAnimator.onAnimationUpdate = {
animView.alpha = alphaAnimator.animatedValue
}
let animatorSet = AnimatorSet()
// animatorSet.playSequential = true // Uncomment this to play animations in order
animatorSet.addAnimator(translateAnimator)
animatorSet.addAnimator(alphaAnimator)
animatorSet.start()
}
}
我认为大部分内容不言而喻。我创建了一个转换 x 并淡出的视图。对于每个动画,您实现 onAnimationUpdate
回调以更改视图中使用的值,例如在本例中的平移 x 和 alpha。
注意:与Android矛盾的是,这里的持续时间和延迟是以秒为单位而不是毫秒。
我们现在正在使用此代码,效果很好!我已经在我们的 Android 应用程序中编写了一些动画内容。我可以轻松地将动画移植到 iOS,只需进行一些最少的重写,并且动画效果完全一样!我可以复制我的问题中编写的代码,将 Kotlin 代码更改为 Swift,应用 onAnimationUpdate
,将持续时间和延迟更改为秒,动画效果非常好。
我想将其作为开源库发布,但我还没有这样做。当我发布它时,我会更新这个答案。
如果您对代码或其工作原理有任何疑问,请随时提问。
我使用 Animator 类 在我的 Android 应用程序中制作了一个相当复杂的动画。我想将此动画移植到 iOS。最好它有点像 Android Animator。我环顾四周,似乎没有什么是我想要的。我得到的最接近的是 CAAnimation。但不幸的是,如果将他们放在一个组中,所有子代表都会被忽略。
让我从我在 Android 上制作的动画开始。我正在为三个视图组制作动画(其中包含一个 ImageView 和一个 TextView)。每个按钮我有一个动画,将视图向左平移,同时将 alpha 设置为 0。在该动画之后,还有另一个动画将相同的视图从右侧平移到原始位置,并将 alpha 设置回 1。除了平移和 alpha 动画之外,还有一个视图还具有缩放动画。所有视图都使用不同的计时功能(缓动)。输入动画和输出动画是不同的,一个视图具有不同的缩放计时功能,而 alpha 和平移动画使用相同的功能。第一个动画结束后,我将设置值以准备第二个动画。缩放动画的持续时间也比平移和 alpha 动画短。我将单个动画(翻译和 alpha)放在 AnimatorSet(基本上是动画组)中。这个 AnimatorSet 被放在另一个 AnimatorSet 中以 运行 彼此之后的动画(第一个动画然后比在)。并且这个 AnimatorSet 被放在另一个 AnimatorSet 中,它 运行 同时为所有 3 个按钮设置动画。
抱歉解释得太长了。但是这样你就会明白我是如何尝试将它移植到 iOS 的。这个对于 UIView.animate() 来说太复杂了。如果放入 CAAnimationGroup,CAAnimation 会覆盖委托。据我所知,ViewPropertyAnimator 不允许自定义计时功能,并且无法协调多个动画。
有人知道我可以用它做什么吗?我也很喜欢自定义实现,它会在每个动画滴答时给我一个回调,这样我就可以相应地更新视图。
编辑
Android动画代码:
fun setState(newState: State) {
if(state == newState) {
return
}
processing = false
val prevState = state
state = newState
val reversed = newState.ordinal < prevState.ordinal
val animators = ArrayList<Animator>()
animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
displayMiddleButtonState()
}))
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
displayLeftButtonState()
}))
}
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(
if(newState == State.TAKE_PICTURE) rightButton else null,
if(newState == State.CROP_PICTURE) rightButton else null,
rightButton.imageView.width.toFloat(),
reversed,
halfAnimationDone = {
displayRightButtonState(inAnimation = true)
}))
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
animatorSet.start()
}
fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animators = ArrayList<Animator>()
if(animateInView != null) {
val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
if(animateOutView == null) {
animateInAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
halfAnimationDone()
}
})
}
animators.add(animateInAnimator)
}
if(animateOutView != null) {
val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
animators.add(animateOutAnimator)
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
return animatorSet
}
private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val fadeDuration = translateDuration
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, fadeAnimator)
return animateSet
}
fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
val animatorSet = AnimatorSet()
animatorSet.playTogether(animateInAnimator, animateOutAnimator)
return animatorSet
}
private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val scaleDuration = 100L
val fadeDuration = translateDuration
val maxTranslationXValue = middleButtonImageView.width.toFloat()
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val scaleValues =
if(animateIn) floatArrayOf(0.8f, 1f)
else floatArrayOf(1f, 0.8f)
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
scaleXAnimator.duration = scaleDuration
scaleYAnimator.duration = scaleDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
}
if(animateIn) {
val scaleStartDelay = translateDuration - scaleDuration
val scaleStartValue = scaleValues[0]
middleButtonImageView.scaleX = scaleStartValue
middleButtonImageView.scaleY = scaleStartValue
scaleXAnimator.startDelay = scaleStartDelay
scaleYAnimator.startDelay = scaleStartDelay
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)
return animateSet
}
编辑 2
这是动画在 Android 上的效果视频:
这是我认为您正在寻找的动画的开始。如果您不喜欢幻灯片的时间安排,则可以将 UIView.animate
换成 .curveEaseInOut
换成 CAKeyframeAnimation
,这样您可以更精细地控制每一帧。您需要为每个要设置动画的视图设置一个 CAKeyFrameAnimation
。
这是一个 playground,您可以将它复制并粘贴到一个空的 playground 中以查看它的运行情况。
import UIKit
import Foundation
import PlaygroundSupport
class ViewController: UIViewController {
let bottomBar = UIView()
let orangeButton = UIButton(frame: CGRect(x: 0, y: 10, width: 75, height: 75))
let yellow = UIView(frame: CGRect(x: 20, y: 20, width: 35, height: 35))
let magenta = UIView(frame: CGRect(x: 80, y: 30, width: 15, height: 15))
let cyan = UIView(frame: CGRect(x: 50, y: 20, width: 35, height: 35))
let brown = UIView(frame: CGRect(x: 150, y: 30, width:
15, height: 15))
let leftBox = UIView(frame: CGRect(x: 15, y: 10, width: 125, height: 75))
func setup() {
let reset = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
reset.backgroundColor = .white
reset.addTarget(self, action: #selector(resetAnimation), for: .touchUpInside)
self.view.addSubview(reset)
bottomBar.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
bottomBar.backgroundColor = .purple
self.view.addSubview(bottomBar)
orangeButton.backgroundColor = .orange
orangeButton.center.x = bottomBar.frame.size.width / 2
orangeButton.addTarget(self, action: #selector(orangeTapped(sender:)), for: .touchUpInside)
orangeButton.clipsToBounds = true
bottomBar.addSubview(orangeButton)
yellow.backgroundColor = .yellow
orangeButton.addSubview(yellow)
magenta.backgroundColor = .magenta
magenta.alpha = 0
orangeButton.addSubview(magenta)
// Left box is an invisible bounding box to get the effect that the view appeared from nowhere
// Clips to bounds so you cannot see the view when it has not been animated
// Try setting to false
leftBox.clipsToBounds = true
bottomBar.addSubview(leftBox)
cyan.backgroundColor = .cyan
leftBox.addSubview(cyan)
brown.backgroundColor = .brown
brown.alpha = 0
leftBox.addSubview(brown)
}
@objc func orangeTapped(sender: UIButton) {
// Perform animation
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
self.yellow.frame = CGRect(x: -20, y: 30, width: 15, height: 15)
self.yellow.alpha = 0
self.magenta.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
self.magenta.alpha = 1
self.cyan.frame = CGRect(x: -150, y: 30, width: 15, height: 15)
self.cyan.alpha = 0
self.brown.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
self.brown.alpha = 1
}, completion: nil)
}
@objc func resetAnimation() {
// Reset the animation back to the start
yellow.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
yellow.alpha = 1
magenta.frame = CGRect(x: 80, y: 30, width: 15, height: 15)
magenta.alpha = 0
cyan.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
cyan.alpha = 1
brown.frame = CGRect(x: 150, y: 30, width: 15, height: 15)
brown.alpha = 0
}
}
let viewController = ViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
viewController.view.backgroundColor = .blue
viewController.setup()
PlaygroundPage.current.liveView = viewController
所以我一直在使用 CADisplayLink
研究我自己的解决方案。文档是这样描述的 CADisplayLink
:
CADisplayLink is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.
它基本上在执行绘图代码时提供回调(因此您可以 运行 您的动画流畅)。
我不打算解释这个答案中的所有内容,因为它会有很多代码,而且大部分应该都很清楚。如果有什么不明白或者你有问题可以在这个答案下面评论。
此解决方案为动画提供了完全的自由,并提供了协调它们的能力。我看了很多 Android 上的 Animator
class 并想要类似的语法,以便我们可以轻松地将动画从 Android 移植到 iOS 或其他方式大约。我已经对其进行了几天的测试,并且也消除了一些怪癖。话不多说,让我们看一些代码吧!
这是 Animator
class,这是动画的基础结构 classes:
class Animator {
internal var displayLink: CADisplayLink? = nil
internal var startTime: Double = 0.0
var hasStarted: Bool = false
var hasStartedAnimating: Bool = false
var hasFinished: Bool = false
var isManaged: Bool = false
var isCancelled: Bool = false
var onAnimationStart: () -> Void = {}
var onAnimationEnd: () -> Void = {}
var onAnimationUpdate: () -> Void = {}
var onAnimationCancelled: () -> Void = {}
public func start() {
hasStarted = true
startTime = CACurrentMediaTime()
if(!isManaged) {
startDisplayLink()
}
}
internal func startDisplayLink() {
stopDisplayLink() // make sure to stop a previous running display link
displayLink = CADisplayLink(target: self, selector: #selector(animationTick))
displayLink?.add(to: .main, forMode: .commonModes)
}
internal func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
@objc internal func animationTick() {
}
public func cancel() {
isCancelled = true
onAnimationCancelled()
if(!isManaged) {
animationTick()
}
}
}
它包含所有重要信息,如启动 CADisplayLink
、提供停止 CADisplayLink
的能力(当动画完成时)、指示状态的布尔值和一些回调。您还会注意到 isManaged 布尔值。此布尔值表示 Animator
由一个组控制。如果是,该组将提供动画滴答声,并且此 class 不应启动 CADisplayLink
。
接下来是 ValueAnimator
:
class ValueAnimator : Animator {
public internal(set) var progress: Double = 0.0
public internal(set) var interpolatedProgress: Double = 0.0
var duration: Double = 0.3
var delay: Double = 0
var interpolator: Interpolator = EasingInterpolator(ease: .LINEAR)
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed: Double = (isCancelled) ? self.duration : CACurrentMediaTime() - startTime - delay
if(elapsed < 0) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
if(duration <= 0) {
progress = 1.0
} else {
progress = min(elapsed / duration, 1.0)
}
interpolatedProgress = interpolator.interpolate(elapsedTimeRate: progress)
updateAnimationValues()
onAnimationUpdate()
if(elapsed >= duration) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
internal func updateAnimationValues() {
}
}
这个class是所有价值动画师的基础class。但如果您想自己进行计算,它也可以用于您自己制作动画。您可能会注意到此处的 Interpolator
和 interpolatedProgress
。 Interpolator
class 稍后会显示。 class 提供了动画的缓动。这就是 interpolatedProgress
的用武之地。progress
只是从 0.0 到 1.0 的线性进展,但 interpolatedProgress
可能具有不同的缓动值。例如,当 progress
的值为 0.2 时,interpolatedProgress
可能已经有 0.4,具体取决于您将使用的缓动。还要确保使用 interpolatedProgress
来计算正确的值。 ValueAnimator
的第一个子 class 示例如下。
下面是 CGFloatValueAnimator
,顾名思义,它对 CGFloat 值进行动画处理:
class CGFloatValueAnimator : ValueAnimator {
private let startValue: CGFloat
private let endValue: CGFloat
public private(set) var animatedValue: CGFloat
init(startValue: CGFloat, endValue: CGFloat) {
self.startValue = startValue
self.endValue = endValue
self.animatedValue = startValue
}
override func updateAnimationValues() {
animatedValue = startValue + CGFloat(Double(endValue - startValue) * interpolatedProgress)
}
}
这是一个如何子 class ValueAnimator
的例子,如果你需要其他的,比如双精度数或整数,你可以做更多这样的例子。您只需提供开始值和结束值,Animator
根据 interpolatedProgress
计算当前的 animatedValue
是什么。您可以使用此 animatedValue
来更新您的观点。我会在最后展示一个例子。
因为我已经多次提到 Interpolator
,所以我们现在继续Interpolator
:
protocol Interpolator {
func interpolate(elapsedTimeRate: Double) -> Double
}
这只是一个您可以自己实现的协议。我将向您展示我自己使用的 EasingInterpolator
class 的一部分。如果有人需要,我可以提供更多。
class EasingInterpolator : Interpolator {
private let ease: Ease
init(ease: Ease) {
self.ease = ease
}
func interpolate(elapsedTimeRate: Double) -> Double {
switch (ease) {
case Ease.LINEAR:
return elapsedTimeRate
case Ease.SINE_IN:
return (1.0 - cos(elapsedTimeRate * Double.pi / 2.0))
case Ease.SINE_OUT:
return sin(elapsedTimeRate * Double.pi / 2.0)
case Ease.SINE_IN_OUT:
return (-0.5 * (cos(Double.pi * elapsedTimeRate) - 1.0))
case Ease.CIRC_IN:
return -(sqrt(1.0 - elapsedTimeRate * elapsedTimeRate) - 1.0)
case Ease.CIRC_OUT:
let newElapsedTimeRate = elapsedTimeRate - 1
return sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate)
case Ease.CIRC_IN_OUT:
var newElapsedTimeRate = elapsedTimeRate * 2.0
if (newElapsedTimeRate < 1.0) {
return (-0.5 * (sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate) - 1.0))
}
newElapsedTimeRate -= 2.0
return (0.5 * (sqrt(1 - newElapsedTimeRate * newElapsedTimeRate) + 1.0))
default:
return elapsedTimeRate
}
}
}
这些只是特定缓动计算的几个例子。我实际上移植了位于此处的 Android 的所有缓动:https://github.com/MasayukiSuda/EasingInterpolator.
在展示示例之前,我还有一个 class 要展示。哪个 class 允许动画师分组:
class AnimatorSet : Animator {
private var animators: [Animator] = []
var delay: Double = 0
var playSequential: Bool = false
override func start() {
super.start()
}
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed = CACurrentMediaTime() - startTime - delay
if(elapsed < 0 && !isCancelled) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
var finishedNumber = 0
for animator in animators {
if(!animator.hasStarted) {
animator.start()
}
animator.animationTick()
if(animator.hasFinished) {
finishedNumber += 1
} else {
if(playSequential) {
break
}
}
}
if(finishedNumber >= animators.count) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
public func addAnimator(_ animator: Animator) {
animator.isManaged = true
animators.append(animator)
}
public func addAnimators(_ animators: [Animator]) {
for animator in animators {
animator.isManaged = true
self.animators.append(animator)
}
}
override func cancel() {
for animator in animators {
animator.cancel()
}
super.cancel()
}
}
如您所见,这里是我设置 isManaged
布尔值的地方。您可以将您制作的多个动画师放入此 class 中以协调它们。因为这个 class 也扩展了 Animator
你也可以放入另一个 AnimatorSet
或多个。默认情况下它 运行 同时播放所有动画,但是如果 playSequential
设置为 true,它将 运行 所有动画按顺序播放。
演示时间:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let animView = UIView()
animView.backgroundColor = UIColor.yellow
self.view.addSubview(animView)
animView.snp.makeConstraints { maker in
maker.width.height.equalTo(100)
maker.center.equalTo(self.view)
}
let translateAnimator = CGFloatValueAnimator(startValue: 0, endValue: 100)
translateAnimator.delay = 1.0
translateAnimator.duration = 1.0
translateAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
translateAnimator.onAnimationStart = {
animView.backgroundColor = UIColor.blue
}
translateAnimator.onAnimationEnd = {
animView.backgroundColor = UIColor.green
}
translateAnimator.onAnimationUpdate = {
animView.transform.tx = translateAnimator.animatedValue
}
let alphaAnimator = CGFloatValueAnimator(startValue: animView.alpha, endValue: 0)
alphaAnimator.delay = 1.0
alphaAnimator.duration = 1.0
alphaAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
alphaAnimator.onAnimationUpdate = {
animView.alpha = alphaAnimator.animatedValue
}
let animatorSet = AnimatorSet()
// animatorSet.playSequential = true // Uncomment this to play animations in order
animatorSet.addAnimator(translateAnimator)
animatorSet.addAnimator(alphaAnimator)
animatorSet.start()
}
}
我认为大部分内容不言而喻。我创建了一个转换 x 并淡出的视图。对于每个动画,您实现 onAnimationUpdate
回调以更改视图中使用的值,例如在本例中的平移 x 和 alpha。
注意:与Android矛盾的是,这里的持续时间和延迟是以秒为单位而不是毫秒。
我们现在正在使用此代码,效果很好!我已经在我们的 Android 应用程序中编写了一些动画内容。我可以轻松地将动画移植到 iOS,只需进行一些最少的重写,并且动画效果完全一样!我可以复制我的问题中编写的代码,将 Kotlin 代码更改为 Swift,应用 onAnimationUpdate
,将持续时间和延迟更改为秒,动画效果非常好。
我想将其作为开源库发布,但我还没有这样做。当我发布它时,我会更新这个答案。
如果您对代码或其工作原理有任何疑问,请随时提问。