Smooth animation CAGradientLayer with long duration

Viewed 133

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.stack.imgur.com/eUvOo.gif

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

2 Answers

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

Your draw method is inserting a new gradient layer every time it is called. But this method will be called repeatedly, resulting in multiple layers being inserted. The only thing you should be doing in draw is stroking/filling paths, and in this case, you probably should not implement draw at all.

The insertion of layers does not belong in draw.

Related