我使用 Animator 类在我的 Android 应用程序中制作了一个相当复杂的动画。我想将此动画移植到 iOS。最好是有点像 Android Animator。我环顾四周,似乎没有什么是我想要的。我得到的最接近的是 CAAnimation。但不幸的是,如果将所有子代表放在一个组中,它们都会被忽略。
让我从我在 Android 上制作的动画开始。我正在动画三个 View 组(其中包含一个 ImageView 和一个 TextView)。每个按钮我有一个动画,它将 View 向左平移,同时将 alpha 动画化为 0。在该动画之后,还有另一个动画将同一 View 从右侧平移到原始位置,并将 alpha 动画化回 1。除了平移和 alpha 动画之外,还有一个 View 还具有缩放动画。所有 View 都使用不同的计时功能(缓动)。动画输入和动画输出是不同的,一个 View 具有不同的缩放计时函数,而 alpha 和平移动画使用相同的时间函数。第一个动画结束后,我正在设置值以准备第二个动画。缩放动画的持续时间也比平移和 alpha 动画短。我将单个动画(平移和 alpha)放在 AnimatorSet(基本上是一组动画)中。这个 AnimatorSet 被放在另一个 AnimatorSet 中以在彼此之后运行动画(第一个动画然后在)。这个 AnimatorSet 放在另一个 AnimatorSet 中,它同时运行所有 3 个按钮的动画。
对不起,很长的解释。但是通过这种方式您可以了解我是如何尝试将其移植到 iOS 的。这个对于 UIView.animate() 来说太复杂了。如果放入 CAAnimationGroup,CAAnimation 会覆盖委托(delegate)。据我所知,ViewPropertyAnimator 不允许自定义计时功能,也不能协调多个动画。
有人知道我可以用什么吗?我也可以使用自定义实现,它为我提供每个动画滴答的回调,以便我可以相应地更新 View 。
编辑
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 上的外观视频:
https://youtu.be/IKAB9A9qHic
最佳答案
所以我一直在使用 CADisplayLink
开发我自己的解决方案.这是文档描述的方式 CADisplayLink
:
CADisplayLink is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.
它基本上提供了何时执行绘图代码的回调(因此您可以流畅地运行动画)。
我不会在这个答案中解释所有内容,因为它会有很多代码,而且大部分应该是清楚的。如果有不清楚的地方或者您有疑问,可以在此答案下方发表评论。
该解决方案为动画提供了完全的自由,并提供了协调它们的能力。我看了很多
Animator
类在 Android 上,并需要类似的语法,以便我们可以轻松地将动画从 Android 移植到 iOS 或其他方式。我已经对其进行了几天的测试,并消除了一些怪癖。但是说得够多了,让我们看看一些代码!这是
Animator
类,这是动画类的基本结构: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
(当动画完成时),指示状态和一些回调的 bool 值。您还会注意到 isManaged bool 值。这个 bool 值是 Animator
由一组控制。如果是,该组将提供动画滴答声,并且此类不应启动 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() {
}
}
此类是所有值动画师的基类。但如果您希望自己进行计算,它也可以用于自己制作动画。您可能会注意到
Interpolator
和 interpolatedProgress
这里。 Interpolator
类将显示在位。这个类提供了动画的缓动。这是哪里interpolatedProgress
进来了。progress
只是从 0.0 到 1.0 的线性进展,但是 interpolatedProgress
可能有不同的缓和值。例如当 progress
值为 0.2,interpolatedProgress
根据您将使用的缓动,可能已经有 0.4 了。还要确保使用 interpolatedProgress
计算正确的值。一个例子和 ValueAnimator
的第一个子类在下面。以下是
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)
}
}
这是如何子类化
ValueAnimator
的示例例如,如果您需要诸如 double 数或整数之类的其他内容,您可以制作更多这样的内容。您只需提供开始和结束值以及 Animator
基于 interpolatedProgress
计算当前是什么animatedValue
是。你可以用这个animatedValue
更新您的 View 。我会在最后展示一个例子。因为我提到了
Interpolator
已经几次了,我们将继续到 Interpolator
现在:protocol Interpolator {
func interpolate(elapsedTimeRate: Double) -> Double
}
这只是一个您可以自己实现的协议(protocol)。我给你看
EasingInterpolator
的一部分我自己用的课。如果有人需要,我可以提供更多。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 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
的地方 bool 值。你可以在这个类中放置多个你制作的动画师来协调它们。并且因为这个类也扩展了 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 并淡出的 View 。对于每个动画,您实现
onAnimationUpdate
用于更改 View 中使用的值的回调,例如在这种情况下平移 x 和 alpha。注:与Android相反,这里的持续时间和延迟以秒为单位而不是毫秒。
我们现在正在使用此代码,效果很好!我已经在我们的 Android 应用程序中编写了一些动画内容。我可以轻松地将动画移植到 iOS,只需进行一些最小的重写,动画效果完全相同!我可以复制我的问题中编写的代码,将 Kotlin 代码更改为 Swift,应用
onAnimationUpdate
, 将持续时间和延迟更改为秒,动画效果很好。我想将它作为开源库发布,但我还没有这样做。当我发布它时,我会更新这个答案。
如果您对代码或其工作方式有任何疑问,请随时提出。
关于iOS 复杂动画协调,如 Android Animator(Set),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50721243/