1

我使用 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 上的样子的视频:

https://youtu.be/IKAB9A9qHic

4

2 回答 2

3

所以我一直在研究自己的解决方案,使用CADisplayLink. 这是文档描述的方式CADisplayLink

CADisplayLink 是一个计时器对象,它允许您的应用程序将其绘图与显示器的刷新率同步。

它基本上提供了执行绘图代码时的回调(因此您可以流畅地运行动画)。

我不会在这个答案中解释所有内容,因为这将是很多代码,而且大部分内容应该很清楚。如果有不清楚的地方或您有任何疑问,可以在此答案下方发表评论。

该解决方案为动画提供了完全的自由,并提供了协调它们的能力。我看了很多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(动画完成时)的能力、指示状态的布尔值和一些回调。您还会注意到 isManaged 布尔值。这个布尔值是当一个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() {

    }
}

这个类是所有值动画师的基类。但如果您想自己进行计算,它也可以用于自己制作动画。你可能会注意到这里的Interpolatorand interpolatedProgressInterpolator稍后将显示该类。此类提供动画的缓动。这就是interpolatedProgress进来的地方。progress只是从 0.0 到 1.0 的线性进度,但interpolatedProgress对于缓动可能有不同的值。例如,当progress值为 0.2时,interpolatedProgress根据您将使用的缓动,可能已经有 0.4。还要确保用于interpolatedProgress计算正确的值。下面是一个示例和第一个子类ValueAnimator

顾名思义,下面是CGFloatValueAnimatorCGFloat 值的动画:

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如果您需要其他类型,例如双精度或整数,您可以制作更多这样的示例。您只需提供一个开始值和结束值,并Animator根据interpolatedProgress当前值进行计算animatedValue。您可以使用它animatedValue来更新您的视图。我会在最后展示一个例子。

因为我Interpolator已经提到过几次,我们将继续到Interpolator现在:

protocol Interpolator {
    func interpolate(elapsedTimeRate: Double) -> Double
}

它只是一个您可以自己实现的协议。我将向您展示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布尔值的地方。您可以将您制作的多个动画师放在此类中以协调它们。而且因为这个类也扩展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,将持续时间和延迟更改为秒,动画就像一个魅力。

我想将它作为一个开源库发布,但我还没有这样做。当我发布它时,我会更新这个答案。

如果您对代码或其工作原理有任何疑问,请随时提问。

于 2018-06-13T19:10:14.587 回答
1

这是我认为您正在寻找的动画的开始。如果您不喜欢幻灯片的时间安排,那么您可以切换到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
于 2018-06-07T13:59:58.370 回答