Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CABasicAnimation creates empty default value copy of CALayer

I have a custom CALayer that draws radial gradients. It works great except during animation. It seems that each iteration of CABasicAnimation creates a new copy of the CALayer subclass with empty, default values for the properties:

debug class

In the screenshot above, you see that CABasicAnimation has created a new copy of the layer and is updating gradientOrigin but none of the other properties have come along for the ride.

This has the result of not rendering anything during the animation. Here's a GIF:

improper

Here's what is should look like:

correct

Here's the animation code:

let animation = CABasicAnimation(keyPath: "gradientOrigin")
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let newOrigin: CGPoint = CGPoint(x: 0, y: triangle.bounds.height/2)
animation.fromValue = NSValue(CGPoint: triangle.gradientLayer.gradientOrigin)
animation.toValue = NSValue(CGPoint: newOrigin)
triangle.gradientLayer.gradientOrigin = newOrigin
triangle.gradientLayer.addAnimation(animation, forKey: nil)

Here's the custom CALayer code:

enum RadialGradientLayerProperties: String {
  case gradientOrigin
  case gradientRadius
  case colors
  case locations
}

class RadialGradientLayer: CALayer {
  var gradientOrigin = CGPoint() {
    didSet { setNeedsDisplay() }
  }
  var gradientRadius = CGFloat() {
    didSet { setNeedsDisplay() }
  }

  var colors = [CGColor]() {
    didSet { setNeedsDisplay() }
  }

  var locations = [CGFloat]() {
    didSet { setNeedsDisplay() }
  }

  override init(){
    super.init()
    needsDisplayOnBoundsChange = true
  }

  required init(coder aDecoder: NSCoder) {
    super.init()
  }

  override init(layer: AnyObject) {
    super.init(layer: layer)
  }

  override class func needsDisplayForKey(key: String) -> Bool {
    if key == RadialGradientLayerProperties.gradientOrigin.rawValue || key == RadialGradientLayerProperties.gradientRadius.rawValue || key == RadialGradientLayerProperties.colors.rawValue || key == RadialGradientLayerProperties.locations.rawValue {
      print("called \(key)")
      return true
    }
    return super.needsDisplayForKey(key)

  }

  override func actionForKey(event: String) -> CAAction? {
    if event == RadialGradientLayerProperties.gradientOrigin.rawValue || event == RadialGradientLayerProperties.gradientRadius.rawValue || event == RadialGradientLayerProperties.colors.rawValue || event == RadialGradientLayerProperties.locations.rawValue {
      let animation = CABasicAnimation(keyPath: event)
      animation.fromValue = self.presentationLayer()?.valueForKey(event)
      return animation
    }
    return super.actionForKey(event)
  }

  override func drawInContext(ctx: CGContext) {

    guard let colorRef = self.colors.first else { return }

    let numberOfComponents = CGColorGetNumberOfComponents(colorRef)
    let colorSpace = CGColorGetColorSpace(colorRef)

    let deepGradientComponents: [[CGFloat]] = (self.colors.map {
      let colorComponents = CGColorGetComponents($0)
      let buffer = UnsafeBufferPointer(start: colorComponents, count: numberOfComponents)
      return Array(buffer) as [CGFloat]
      })

    let flattenedGradientComponents = deepGradientComponents.flatMap({ $0 })

    let gradient = CGGradientCreateWithColorComponents(colorSpace, flattenedGradientComponents, self.locations, self.locations.count)

    CGContextDrawRadialGradient(ctx, gradient, self.gradientOrigin, 0, self.gradientOrigin, self.gradientRadius, .DrawsAfterEndLocation)

  }
}
like image 571
Ben Morrow Avatar asked Dec 18 '22 15:12

Ben Morrow


1 Answers

Figured out the answer!

In init(layer:) you have to copy the property values to your class manually. Here's how that looks in action:

override init(layer: AnyObject) {
  if let layer = layer as? RadialGradientLayer {
    gradientOrigin = layer.gradientOrigin
    gradientRadius = layer.gradientRadius
    colors = layer.colors
    locations = layer.locations
  }
  super.init(layer: layer)
}
like image 117
Ben Morrow Avatar answered May 16 '23 10:05

Ben Morrow