iOS 复杂动画协调,如 Android Animator(Set)

标签 ios swift ios-animations

我使用 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() {

    }
}

此类是所有值动画师的基类。但如果您希望自己进行计算,它也可以用于自己制作动画。您可能会注意到 InterpolatorinterpolatedProgress这里。 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/

相关文章:

ios - UIView 在变换动画之前具有自动布局约束 "jumps"

ios - 需要帮助使用 AVAudioPlayer iOS 从 URL 流式传输音频

ios - 重播套件未记录父应用程序中的第二个应用程序

ios - cellForRowAtIndexPath 无法正常工作

arrays - 将数组插入数组

ios - 如何在 segue 目标 View Controller 中执行 segue

ios - 动画 UIWebView 在动画期间不响应触摸

ios - 如何在 subview Controller 的 View 上设置 View ?

php - 使用 PHP 将 MySQL 查询输出为 SQLite

ios - 使用 CABasicAnimation(CAAnimation) 自定义交互过渡