CALayer动画beginTime

时间:2017-07-09 13:37:51

标签: ios calayer caanimation

我正在为iOS开发一个关键帧动画编辑器,允许用户创建简单的动画并查看它们,并根据时间轴绘制。用户可以拖动时间轴以更改当前时间。

这意味着我需要能够在用户指定的时间启动动画。

虽然我可以实现这种行为,但每次重新添加动画到图层时,我也会遇到恼人的故障。在CoreAnimation遵循开始时间之前,此毛刺会导致动画的第一帧非常快速地闪烁。我可以通过在刷新之前将layer.alpha设置为0并且在刷新之后将1设置为0来缓解这种影响,但这仍然会导致令人讨厌的单帧(ish)闪烁!

我重新创建了一个示例视图控制器,它演示了“更改正在运行的动画的时间”所需的代码,但是由于这个项目非常简单,所以你并没有真正看到flush的负面影响: https://gist.github.com/chrisbirch/5cafca50804cf9d778ccd0fdc9e68d56

代码背后的基本思想如下:

每次用户更改当前时间时,我都会重新启动动画,并按照这样的方式调整CALayer计时属性(addStoppedAnimation:第215行):

ani = createGroup()

animatableLayer.speed = 0

animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = 0
animatableLayer.beginTime = animatableLayer.superlayer!.convertTime(CACurrentMediaTime(), from: nil) - time

CATransaction.flush()
animatableLayer.timeOffset = time// offset
print("Time changed \(time)")

故障是由于我在设置timeOffset之前必须调用CATransaction.flush引起的。如果没有调用此刷新,则会导致开始时间被忽略。

我觉得我已经在整个互联网上寻找解决方案来解决这个问题但是我觉得我已经陷入了困境。

我的问题是:

任何人都可以解释为什么我需要调用CATransaction.flush以便我设置的beginTime值生效?看看苹果代码,我没有看到他们为此目的使用同花顺,所以也许我有一些明显的错误!

非常感谢提前

克里斯

1 个答案:

答案 0 :(得分:1)

使用gist中的测试代码,我已经更新了它以检查动画,因此无需读取动画。您可以使用我们的唯一ID来跟踪所有动画并将其存储在具有视图属性的字典中。我没有实现这部分,但我就是这样做的。希望我理解你的问题。我也使用Xcode 9,我不确定代码差异。我改变了一些逻辑片段,所以让我知道这是否解决了这个问题。

UUID().uuidString //for unique string in implementation

//from your code just slightly altered.
//
//  ViewController.swift
//  CATest


//
//  CATestViewController.swift
//  SimpleCALayerTest


import UIKit

class CATestViewController: UIViewController, CAAnimationDelegate{


    var slider : UISlider!
    var animatableLayer : CALayer!

    var animationContainerView : UIView!

    var centerY : CGFloat!
    var startTranslationX : CGFloat!
    var endTranslationX : CGFloat!

    let duration = 10.0

    ///boring nibless view setup code
    override func loadView() {
        let marginX = CGFloat(10)
        let marginY = CGFloat(10)
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

        view.backgroundColor = .lightGray

        slider = UISlider(frame: CGRect(x: marginX, y: 0, width: 200, height: 50))
        slider.maximumValue = Float(duration)
        slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)

        slider.addTarget(self, action: #selector(sliderDragStart(_:)), for: .touchDown)
        slider.addTarget(self, action: #selector(sliderDragEnd(_:)), for: .touchUpInside)

        //A view to house an animated sublayer
        animationContainerView = UIView(frame: CGRect(x: marginX, y: 50, width: 200, height: 70))



        //add a play button that will allow the animation to be played without hindrance from the slider
        let playButton = UIButton(frame: CGRect(x: marginX, y: animationContainerView.frame.maxY + marginY, width: 200, height: 50))
        playButton.setTitle("Change Frame", for: .normal)
        playButton.addTarget(self, action: #selector(playAnimation), for: .touchUpInside)
        view.addSubview(playButton)

        //add a stopped ani button that will allow the animation to be played using slider
        let addStoppedAniButton = UIButton(frame: CGRect(x: playButton.frame.origin.x, y: playButton.frame.maxY + marginY, width: playButton.frame.width, height: playButton.frame.size.height))
        addStoppedAniButton.setTitle("Pause", for: .normal)
        addStoppedAniButton.addTarget(self, action: #selector(cmPauseTapped(_:)), for: .touchUpInside)
        view.addSubview(addStoppedAniButton)



        let animatableLayerWidth = animationContainerView.bounds.width / CGFloat(4)
        centerY = animationContainerView.bounds.midY
        startTranslationX = animatableLayerWidth / CGFloat(2)
        endTranslationX = animationContainerView.bounds.width - animatableLayerWidth / CGFloat(2)


        animationContainerView.backgroundColor = .white
        animationContainerView.layer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
        animationContainerView.layer.borderWidth = 1

        view.addSubview(slider)
        view.addSubview(animationContainerView)

        //Now add a layer to animate to the container
        animatableLayer = CALayer()
        animatableLayer.backgroundColor = UIColor.yellow.cgColor
        animatableLayer.borderWidth = 1
        animatableLayer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
        var r = animationContainerView.bounds.insetBy(dx: 0, dy: 4)
        r.size.width = animatableLayerWidth
        animatableLayer.frame = r
        animationContainerView.layer.addSublayer(animatableLayer)

        self.view = view
    }

    @objc func cmPauseTapped(_ sender : UIButton){
        if animatableLayer.speed == 0{
            resume()
        }else{
            pause()
        }
    }

    @objc func sliderChanged(_ sender: UISlider){
        if animatableLayer.speed == 0{
            let time = CFTimeInterval(sender.value)
            animatableLayer.speed = 0
            animatableLayer.timeOffset = time// offset
            print("Time changed \(time)")
        }
    }

    var animations = [CAAnimation]()

    func addAnimations(){
        let ani = CAAnimation()
        animations.append(ani)
    }

    @objc func sliderDragStart(_ sender: UISlider)
    {
        if animatableLayer.speed > 0{
            animatableLayer.speed = 0
        }
        addStoppedAnimation()
    }

    func pause(){
        //just updating slider
        if slider.value != Float(animatableLayer.timeOffset){
            UIView.animate(withDuration: 0.3, animations: {
                self.slider.setValue(Float(self.animatableLayer.timeOffset), animated: true)
            })
        }

        animatableLayer.timeOffset = animatableLayer.convertTime(CACurrentMediaTime(), from: nil)
        animatableLayer.speed = 0

    }

    func resume(){
        if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
            animatableLayer.speed = 1.0;
            let pausedTime = animatableLayer.timeOffset
            animatableLayer.beginTime = 0.0;
            let timeSincePause = animatableLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
            animatableLayer.beginTime = timeSincePause;
            return
        }

        print("Drag End with need to readd animation")
        ani = createGroup()
        animatableLayer.speed = 1
        animatableLayer.add(ani, forKey: "an animation key")
        let time = CFTimeInterval(slider.value)
        animatableLayer.timeOffset = time
        animatableLayer.beginTime = CACurrentMediaTime()

    }
    @objc func sliderDragEnd(_ sender: UISlider){
        resume()
    }

    //Animations

    var ani : CAAnimationGroup!

    func createGroup() -> CAAnimationGroup
    {
        let ani = CAAnimationGroup()

        ani.isRemovedOnCompletion = false
        ani.duration = 10
        ani.delegate = self

        ani.animations = [createTranslationAnimation(),createColourAnimation()]
        return ani
    }
    func createTranslationAnimation() -> CAKeyframeAnimation
    {
        let ani = CAKeyframeAnimation(keyPath: "position")
        ani.delegate = self
        ani.isRemovedOnCompletion = false
        ani.duration = 10
        ani.values = [CGPoint(x:0,y:centerY),CGPoint(x:endTranslationX,y:centerY)]
        ani.keyTimes = [0,1]
        return ani
    }

    func createColourAnimation() -> CAKeyframeAnimation
    {
        let ani = CAKeyframeAnimation(keyPath: "backgroundColor")
        ani.delegate = self
        ani.isRemovedOnCompletion = false

        ani.duration = 10
        ani.values = [UIColor.red.cgColor,UIColor.blue.cgColor]
        ani.keyTimes = [0,1]

        return ani
    }

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        print("Animation Stopped")
    }

    func animationDidStart(_ anim: CAAnimation) {
        print("Animation started")
    }

    func addStoppedAnimation()
    {
        if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
            slider.value += 0.5
            sliderChanged(slider)
            return
            //we do not want to readd it
        }
        ani = createGroup()
        animatableLayer.speed = 0
        animatableLayer.add(ani, forKey: "an animation key")
        let time = CFTimeInterval(slider.value)
        animatableLayer.timeOffset = time
        animatableLayer.beginTime = CACurrentMediaTime()
    }

    @objc func playAnimation(){
      addStoppedAnimation()
    }

}