像Android Animator(Set)

时间:2018-06-06 13:08:17

标签: ios swift ios-animations

我使用Animator课程在我的Android应用中制作了一个相当复杂的动画。我想将此动画移植到iOS。最好是有点像Android Animator。我环顾四周,似乎没有什么是我想要的。我得到的最接近的是CAAnimation。但不幸的是,如果他们被安排在一个小组中,所有儿童代表都会被忽视。

让我先从我在Android上制作的动画开始。我动画了三个视图组(包含ImageView和TextView)。每个按钮我有一个动画,它将视图转换为左边,同时将alpha动画为0.在该动画之后,还有另一个动画,它将同一视图从右侧转换为原始位置,并将alpha动画回1。除了翻译和alpha动画之外,还有一个视图还具有缩放动画。所有视图都使用不同的计时功能(缓动)。动画和动画输出是不同的,一个视图具有不同的缩放计时功能,而alpha和translate动画使用相同的。第一个动画结束后,我设置值以准备第二个动画。缩放动画的持续时间也比翻译和alpha动画短。我将单个动画(translate和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

2 个答案:

答案 0 :(得分:1)

这是我认为你正在寻找的动画的开始。如果您不喜欢幻灯片的时间安排,那么您可以将UIView.animate.curveEaseInOut切换为CAKeyframeAnimation,您可以更精细地控制每一帧。对于要动画的每个视图,您需要CAKeyFrameAnimation

enter image description here

这是一个游乐场,您可以将其复制并粘贴到空的操场上,以便查看它的实际效果。

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

答案 1 :(得分:1)

所以我一直在使用CADisplayLink开发自己的解决方案。这就是文档描述CADisplayLink

的方式
  

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

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

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

此解决方案为动画提供了完全的自由,并提供了协调它们的能力。我在Android上的Animator课上看了很多,想要类似的语法,这样我们就可以轻松地将动画从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 boolean。这个布尔值是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() {

    }
}

此类是所有值动画师的基类。但如果您希望自己进行计算,它也可以用于自己动画。您可能会在此注意到InterpolatorinterpolatedProgressInterpolator类将稍微显示出来。此类提供动画的缓动。这是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的一个例子,如果你需要像双打或整数这样的其他人,你可以做更多这样的事情。您只需提供一个起始值和结束值,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。默认情况下,它会同时运行所有动画,但如果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() } } 设置为true,它将按顺序运行所有动画。

演示时间:

onAnimationUpdate

我认为大部分内容都不言自明。我创建了一个转换x并淡出的视图。对于每个动画,您实现onAnimationUpdate回调以更改视图中使用的值,例如转换x和alpha。

注意:与Android相反,持续时间和延迟以秒为单位,而不是毫秒。

我们正在使用此代码,效果很好!我已经在我们的Android应用中写了一些动画内容。我可以轻松地将动画移植到iOS,并进行一些最小的重写,动画的工作方式完全相同!我可以复制我的问题中写的代码,将Kotlin代码更改为Swift,应用{{1}},将持续时间和延迟更改为秒,动画就像魅力一样。

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

如果您对代码或代码有任何疑问,请随时提问。