Core Animation - modify animation property

2019-01-29 06:28发布

问题:

I have animation

 func startRotate360() {
    let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
    rotation.fromValue = 0
    rotation.toValue =  Double.pi * 2
    rotation.duration = 1
    rotation.isCumulative = true
    rotation.repeatCount = Float.greatestFiniteMagnitude
    self.layer.add(rotation, forKey: "rotationAnimation")
}

What I want is ability to stop animation by setting its repeat count to 1, so it completes current rotation (simply remove animation is not ok because it looks not good)

I try following

func stopRotate360() {
    self.layer.animation(forKey: "rotationAnimation")?.repeatCount = 1
}

But I get crash and in console

attempting to modify read-only animation

How to access writable properties ?

回答1:

Give this a go. You can in fact change CAAnimations that are in progress. There are so many ways. This is the fastest/simplest. You could even stop the animation completely and resume it without the user even noticing.

You can see the start animation function along with the stop. The start animation looks similar to yours while the stop grabs the current rotation from the presentation layer and creates an animation to rotate until complete. I also smoothed out the duration to be a percentage of the time needed to complete based on current rotation z to full rotation based on the running animation. Then I remove the animation with the repeat count and add the new animation. You can see the view rotate smoothly to the final position and stop. You will get the idea. Drop it in and run it and see what you think. Hit the button to start and hit it again to see it finish rotation and stop.

import UIKit

class ViewController: UIViewController {

    var animationView = UIView()
    var button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        animationView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        animationView.backgroundColor = .green
        animationView.center = view.center
        self.view.addSubview(animationView)

        let label = UILabel(frame: animationView.bounds)
        label.text = "I Spin"
        animationView.addSubview(label)


        button = UIButton(frame: CGRect(x: 20, y: animationView.frame.maxY + 60, width: view.bounds.width - 40, height: 40))
        button.setTitle("Animate", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(ViewController.pressed), for: .touchUpInside)
        self.view.addSubview(button)

    }

    func pressed(){

        if let title = button.titleLabel?.text{
            let trans = CATransition()
            trans.type = "rippleEffect"
            trans.duration = 0.6
            button.layer.add(trans, forKey: nil)

            switch title {
            case "Animate":
                //perform animation
                button.setTitle("Stop And Finish", for: .normal)
                rotateAnimationRepeat()
                break
            default:
                //stop and finish
                button.setTitle("Animate", for: .normal)
                stopAnimationAndFinish()
                break
            }
        }

    }

    func rotateAnimationRepeat(){
        //just to be sure because of how i did the project
        animationView.layer.removeAllAnimations()
        let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotation.fromValue = 0
        rotation.toValue =  Double.pi * 2
        rotation.duration = 0.5
        rotation.repeatCount = Float.greatestFiniteMagnitude
        //not doing cumlative
        animationView.layer.add(rotation, forKey: "rotationAnimation")
    }

    func stopAnimationAndFinish(){
        if let presentation = animationView.layer.presentation(){
            if let currentRotation = presentation.value(forKeyPath: "transform.rotation.z") as? CGFloat{

                var duration = 0.5

                //smooth out duration for change
                duration = Double((CGFloat(Double.pi * 2) - currentRotation))/(Double.pi * 2)
                animationView.layer.removeAllAnimations()

                let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
                rotation.fromValue = currentRotation
                rotation.toValue = Double.pi * 2
                rotation.duration = duration * 0.5
                animationView.layer.add(rotation, forKey: "rotationAnimation")

            }
        }
    }
}

Result:



回答2:

Maybe this is not the best solution but it works, as you say you can not modify properties of the CABasicAnimation once is created, also we need to remove the rotation.repeatCount = Float.greatestFiniteMagnitude, if notCAAnimationDelegatemethodanimationDidStop` is never called, with this approach the animation can be stoped without any problems as you need

step 1: first declare a variable flag to mark as you need stop animation in your custom class

var needStop : Bool = false

step 2: add a method to stopAnimation after ends

func stopAnimation()
{
    self.needStop = true
}

step 3: add a method to get your custom animation

func getRotate360Animation() ->CAAnimation{
    let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
    rotation.fromValue = 0
    rotation.toValue =  Double.pi * 2
    rotation.duration = 1
    rotation.isCumulative = true
    rotation.isRemovedOnCompletion = false
    return rotation
}

step 4: Modify your startRotate360 func to use your getRotate360Animation() method

func startRotate360() {
    let rotationAnimation = self.getRotate360Animation()
    rotationAnimation.delegate = self
    self.layer.add(rotationAnimation, forKey: "rotationAnimation")
}

step 5: Implement CAAnimationDelegate in your class

extension YOURCLASS : CAAnimationDelegate
{

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        if(anim == self.layer?.animation(forKey: "rotationAnimation"))
        {
            self.layer?.removeAnimation(forKey: "rotationAnimation")
            if(!self.needStop){
                let animation = self.getRotate360Animation()
                animation.delegate = self
                self.layer?.add(animation, forKey: "rotationAnimation")
            }
        }
    }
}

This works and was tested

Hope this helps you