Search code examples
swiftanimationcalayercabasicanimation

Smooth animation CAGradientLayer with long duration


Created CAGradientLayer with a sharp color transition. Position of the color transition changes during the time with using CABasicAnimation.

Everything works, but if I use long duration, animation doesn't produce a smooth change.

Code of gradient creation

override func draw(_ rect: CGRect) {
        
        let path = CGMutablePath()
        path.addRect(CGRect(x: 0, y: 0, width: 50, height: 50))
        path.move(to: CGPoint(x: 75, y: 75))
        path.addRect(CGRect(x: 75, y: 75, width: 50, height: 50))
        
        let mask = CAShapeLayer()
        mask.path = path
        
        gradient = CAGradientLayer()
        gradient.mask = mask
        gradient.colors = [UIColor.orange.cgColor, UIColor.blue.cgColor]
        gradient.locations = [0, 0]
        gradient.startPoint = CGPoint(x: 0, y: 0)
        gradient.endPoint = CGPoint(x: 1, y: 0)
        gradient.frame = bounds
        
        layer.insertSublayer(gradient, at: 0)
    }

Animation code

func play() {
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [0, 0]
        animation.toValue = [1, 1]
        animation.duration = 120
        animation.fillMode = .backwards
        animation.isRemovedOnCompletion = false
        gradient.add(animation, forKey: nil)
    }

Demonstration of how the code works https://i.sstatic.net/eUvOo.gif

Are there any options to make the changes smooth with long animation duration?


Solution

  • This looks like a limitation of CAGradientLayer

    You can check current progress of CABasicAnimation using layer presentation().

    And using CADisplayLink you can check updated value which presentation layer has for each frame, something like this:

        ...
        gradient.add(animation, forKey: nil)
    
        let link = CADisplayLink(target: self, selector: #selector(linkHandler))
        link.add(to: .main, forMode: .default)
        link.isPaused = false
    }
    
    @objc func linkHandler() {
        NSLog("\(gradient.presentation()?.locations)")
    }
    

    From this test you'll see that value gest updated totally fine, but CAGradientLayer rounds them to around 0.004. Imagine that inside there's code like

    locations = locations.map { round($0 * 250) / 250 }
    

    I doubt there's anything you can do about that.

    What you can do is use Metal do draw gradient by hand. It's a big and kind of scary topic, but drawing gradient isn't that hard. Check out Rainbows, it's outdated you will have to update the syntax to the current one, but it should be possible