How to rotate a CAShapeLayer around some point with animation?

124 Views Asked by At

A custom view has some CALayers as its content.

picture

I want to add a consistent rotation animation for the red line. (imagine it as pointer of an analog clock)

Expected result: the red line rotates at the center of the blue square.

Current result: the red line rotates at the left bottom corner of the blue square. See gif below.

gif


The animation code for the red line is :

let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
    
// Set animation properties
rotationAnimation.fromValue = CGFloat.pi * 2.0
rotationAnimation.toValue = 0
rotationAnimation.duration = 2.0
rotationAnimation.isCumulative = true
rotationAnimation.repeatCount = Float.greatestFiniteMagnitude
    
// Add rotation animation to pointer layer
pointerLayer.add(rotationAnimation, forKey: "rotationAnimation")

The complete project is here:

https://github.com/cool8jay/public/files/11426695/testDrawPointer.zip


Here are what I have tried after google or stackoverflow online:

  1. change anchorPoint
  2. use CATransform3D
  3. use CGAffineTransform
  4. combinations of above

None works for me.

1

There are 1 best solutions below

0
ix4n33 On BEST ANSWER

You forgot to set the frame of your layers, add these lines to your code and it will works:

borderLayer.frame = bounds
pointerLayer.frame = borderLayer.bounds

Leo's answer works, too. The reason you are seeing the red pointer stretch out the blue border is because the color and shape you use, combine with the stroke line's parallel drawing and bounds clipping, can easily create optical illusion. So it would looks like its not rotating around the center but in fact it do.

Here are the complete code:

class ClockView: NSView {
    
    let pointerLayer = CAShapeLayer()
    let borderLayer = CAShapeLayer()
    
    override public init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    private func setup() {
        self.wantsLayer = true

        borderLayer.frame = bounds
        borderLayer.fillColor = NSColor.gray.cgColor
        borderLayer.path = CGPath(ellipseIn: borderLayer.bounds, transform: nil)
        layer?.addSublayer(borderLayer)
        
        let path = CGMutablePath()
        path.move(to: CGPointMake(bounds.midX, bounds.midY))
        path.addLine(to: CGPointMake(bounds.midX + 50, bounds.midY))
        pointerLayer.path = path
        pointerLayer.frame = bounds
        pointerLayer.lineWidth = 8
        pointerLayer.strokeColor = NSColor.red.cgColor
        layer?.addSublayer(pointerLayer)

        let animation = CABasicAnimation(keyPath: "transform.rotation.z")
        animation.fromValue = CGFloat.pi * 2
        animation.toValue = 0
        animation.duration = 8
        animation.isCumulative = true
        animation.repeatCount = Float.greatestFiniteMagnitude
        pointerLayer.add(animation, forKey: "rotationAnimation")
    }
}

I change the borderLayer's shape to a circle, get rid of the line stroke, as well as changing the color to a least dazzling one.

I also rearrange the code a bit so its easier to read.