2

I want to animate my thumb layer on the drawn path. I can animate the progress layer as per the value but, moving the entire layer doesn't work.

enter image description here

func setProgress(_ value:Double, _ animated :Bool) {
    let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
    progressAnimation.duration = animated ? 0.6 : 0.0
    progressAnimation.fromValue = currentValue
    progressAnimation.toValue = value
    progressAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
    progressLayer.strokeEnd = CGFloat(value)
    progressLayer.add(progressAnimation, forKey: "animateprogress")
}

Using this function I can animate the progress layer. and I'm stuck on animating thumb position.

I tried some things but, it's not working.

failed attempt 1

    guard let path = thumbLayer.path else {return}
    let center = CGPoint(x: frame.width/2, y: frame.height/2)
    let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
    let thumbAngle = 2 * .pi * currentValue - (.pi/2)
    let thumbX = CGFloat(cos(thumbAngle)) * radius
    let thumbY = CGFloat(sin(thumbAngle)) * radius
    let newThumbCenter = CGPoint(x: center.x + thumbX, y: center.y + thumbY)
    let thumbPath = UIBezierPath(arcCenter: newThumbCenter, radius: thumbWidth/2, startAngle: -.pi/2, endAngle: .pi*1.5, clockwise: true)

    let thumbAnimation = CABasicAnimation(keyPath: "path")
    thumbAnimation.duration = animated ? 0.6 : 0.0
    thumbAnimation.fromValue = path
    thumbAnimation.toValue = thumbPath.cgPath
    thumbAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
    thumbLayer.strokeEnd = CGFloat(value)
    thumbLayer.add(thumbAnimation, forKey: "animateprogress")

failed attempt 2

    let intialPosition = thumbLayer.position
    let center = CGPoint(x: frame.width/2, y: frame.height/2)
    let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
    let thumbAngle = 2 * .pi * currentValue - (.pi/2)
    let thumbX = CGFloat(cos(thumbAngle)) * radius
    let thumbY = CGFloat(sin(thumbAngle)) * radius
    let newThumbCenter = CGPoint(x: center.x + thumbX - thumbCenter.x, y: center.y + thumbY - thumbCenter.y)

    thumbLayer.position.x = newThumbCenter.x
    let thumbAnimationX = CABasicAnimation(keyPath: "progress.x")
    thumbAnimationX.duration = animated ? 0.6 : 0.0
    thumbAnimationX.fromValue = intialPosition.x
    thumbAnimationX.toValue = newThumbCenter.x
    thumbAnimationX.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
    thumbLayer.add(thumbAnimationX, forKey: "progress.x")


    thumbLayer.position.y = newThumbCenter.y
    let thumbAnimationY = CABasicAnimation(keyPath: "progress.y")
    thumbAnimationY.duration = animated ? 0.6 : 0.0
    thumbAnimationY.fromValue = intialPosition.y
    thumbAnimationY.toValue = newThumbCenter.y
    thumbAnimationY.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
    thumbLayer.add(thumbAnimationY, forKey: "progress.y")
rv7284
  • 1,092
  • 9
  • 25

1 Answers1

11

In your first attempt, you're trying to animate the thumb's path (the outline of the red circle). In your second attempt, you're trying to animate the position of thumbLayer (but you animated the wrong key paths).

Since you're having difficulty making either of those work, let's try a different approach. Take a look at the standard Swiss railway clock:

Swiss railway clock

Specifically, look at the red dot at the end of the second hand. How does the clock move that red dot around the face? By rotating the second hand around the center of the face.

That's what we're going to do, but we won't draw the whole second hand. We'll just draw the red dot at the end.

We'll put the origin of the thumbLayer's coordinate system at the center of the circular track, and we'll set its path so that the thumb appears on the path. Here's the layout:

layout

Here's code to set up a circleLayer and a thumbLayer:

    private let circleLayer = CAShapeLayer()
    private let thumbLayer = CAShapeLayer()

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let circleRadius: CGFloat = 100
        let thumbRadius: CGFloat = 22
        let circleCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)

        circleLayer.fillColor = nil
        circleLayer.strokeColor = UIColor.black.cgColor
        circleLayer.lineWidth = 4
        circleLayer.lineJoin = .round
        circleLayer.path = CGPath(ellipseIn: CGRect(x: -circleRadius, y: -circleRadius, width: 2 * circleRadius, height: 2 * circleRadius), transform: nil)
        circleLayer.position = circleCenter
        view.layer.addSublayer(circleLayer)

        thumbLayer.fillColor = UIColor.red.cgColor
        thumbLayer.strokeColor = nil
        thumbLayer.path = CGPath(ellipseIn: CGRect(x: -thumbRadius, y: -circleRadius - thumbRadius, width: 2 * thumbRadius, height: 2 * thumbRadius), transform: nil)
        thumbLayer.position = circleCenter
        view.layer.addSublayer(thumbLayer)
    }

With this layout, we can move the thumb along the track without changing its position. Instead, we rotate thumbLayer around its origin:

    @IBAction func buttonWasTapped() {
        let animation = CABasicAnimation(keyPath: "transform.rotation")
        animation.fromValue = 0
        animation.toValue = 2 * CGFloat.pi
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        animation.duration = 1.5
        animation.isAdditive = true
        thumbLayer.add(animation, forKey: nil)
    }

Result:

demo

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    If I stop the rotation at a midpoint. thumb moves back to its original position. Do I need to set the position of the thumb? – rv7284 Oct 18 '18 at 06:18
  • Set the position of the thumb by setting the `affineTransform` of the layer to the appropriate rotation matrix. – rob mayoff Oct 18 '18 at 07:26