【问题标题】:CALayer animation beginTimeCALayer 动画开始时间
【发布时间】:2017-07-09 13:37:51
【问题描述】:

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

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

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

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

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

每次用户更改当前时间时,我都会重新启动动画并像这样摆弄 CALayer 计时属性(addStoppedAnimation:line 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 值生效?查看苹果代码,我从未见过他们为此使用flush,所以也许我有一些明显的错误!

在此先感谢

克里斯

【问题讨论】:

  • 你每次都在看动画吗?为什么不直接更新偏移量?
  • 我提供的示例只是一个非常简单的演示,在我的真实应用程序中有很多动画,并且动画可能甚至没有运行,因为用户可以将当前时间设置为从 0 到“无限”。
  • 为什么不检查动画并更新 timeOffset 如果动画没有运行重新添加动画?为每个动画分配一个唯一的键。将键保存在数组或字典中?

标签: ios calayer caanimation


【解决方案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()
    }

}

【讨论】:

  • 不错!非常感谢。我没有走这条路的原因是因为我有几个用例。问题是,我的应用程序允许用户在播放动画时拖动动画的“关键帧”,因此我将不得不重新启动这些动画。您认为您的上述解决方案是否可以针对重新启动“已更改”动画进行定制?
  • 是的,我能理解。我会在字典中分配一个唯一的键和视图,并使用 map 并根据所有动画键检查字典中的键以进行更新或读取。自己编写整洁的代码。
  • 是的,但是我会遇到与我想的原始代码相同的问题?
  • 动画是否允许预设?
  • 不,它们完全由用户指定。关键帧编辑器是大型应用程序的一部分
猜你喜欢
  • 2021-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-19
  • 2015-10-02
  • 2023-04-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多