Theo Kallioras
Theo Kallioras

Reputation: 3677

Animate CAShapeLayer path with CASpringAnimation

enter image description here

I have a custom UIView where I add a CAShapeLayer as a direct sublayer of the view's layer:

private let arcShapeLayer = CAShapeLayer()

The CAShapeLayer has the following properties (the border is to visualize more easily the animation) declared in the awakeFromNib:

arcShapeLayer.lineWidth = 2.0
arcShapeLayer.strokeColor = UIColor.black.cgColor
arcShapeLayer.fillColor = UIColor.lightGray.cgColor
arcShapeLayer.masksToBounds = false
layer.masksToBounds = false
layer.addSublayer(arcShapeLayer)

I declare the frame of the arcShapeLayer in the layoutSubviews:

arcShapeLayer.frame = layer.bounds

Then I have the following function where I pass a certain percentage (from the ViewController) in order to achieve the arc effect after some dragging. Simply put I add a quadCurve at the bottom of the layer:

func configureArcPath(curvePercentage: CGFloat) -> CGPath {

    let path = UIBezierPath()
    path.move(to: CGPoint.zero)
    path.addLine(to:
        CGPoint(
            x: arcShapeLayer.bounds.minX,
            y: arcShapeLayer.bounds.maxY
        )
    )

    path.addQuadCurve(
        to:
        CGPoint(
            x: arcShapeLayer.bounds.maxX,
            y: arcShapeLayer.bounds.maxY
        ),
        controlPoint:
        CGPoint(
            x: arcShapeLayer.bounds.midX,
            y: arcShapeLayer.bounds.maxY + arcShapeLayer.bounds.maxY * (curvePercentage * 0.4)
        )
    )

    path.addLine(to:
        CGPoint(
            x: arcShapeLayer.bounds.maxX,
            y: arcShapeLayer.bounds.minY
        )
    )
    path.close()

    return path.cgPath

}

Then I try to animate with a spring effect the arc with the following code:

func animateArcPath() {

    let springAnimation = CASpringAnimation(keyPath: "path")
    springAnimation.initialVelocity = 10
    springAnimation.mass = 10
    springAnimation.duration = springAnimation.settlingDuration
    springAnimation.fromValue = arcShapeLayer.path
    springAnimation.toValue = configureArcPath(curvePercentage: 0.0)
    springAnimation.fillMode = .both
    arcShapeLayer.add(springAnimation, forKey: nil)

    arcShapeLayer.path = configureArcPath(curvePercentage: 0.0)

}

The problem, as you can see in the video, is that the arc never overshoots. Although it oscillates between its original position and the rest position, the spring effect is never achieved.

What am I missing here?

Upvotes: 4

Views: 1056

Answers (1)

matt
matt

Reputation: 534925

I played around with this and I can report that you are absolutely right. It has nothing to do clamping at zero. The visible animation clamps at both extremes.

Let's say you supply a big mass value so that the overshoot should go way past the initial position of the convex bow on its return journey back to the start. Then what we see is that the animation clamps both at that original position and at the minimum you are trying to animate to. As the spring comes swinging between them, we see the animation happening between one extreme and the other, but as it reaches the max or min it just clamps there until it has had time to swing to its full extent and return.

I have to conclude that CAShapeLayer path doesn't like springing animations. This same point is raised at CAShapeLayer path spring animation not 'overshooting'

I was able to simulate the sort of look you're probably after by chaining normal basic animations:

enter image description here

Here's the code I used (based on your own code):

@IBAction func animateArcPath() {
    self.animate(to:-1, in:0.5, from:arcShapeLayer.path!)
}
func animate(to arc:CGFloat, in time:Double, from current:CGPath) {
    let goal = configureArcPath(curvePercentage: arc)
    let anim = CABasicAnimation(keyPath: "path")
    anim.duration = time
    anim.fromValue = current
    anim.toValue = goal
    anim.delegate = self
    anim.setValue(arc, forKey: "arc")
    anim.setValue(time, forKey: "time")
    anim.setValue(goal, forKey: "pathh")
    arcShapeLayer.add(anim, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let arc = anim.value(forKey:"arc") as? CGFloat,
        let time = anim.value(forKey:"time") as? Double,
        let p = anim.value(forKey:"pathh") {
        if time < 0.05 {
            return
        }
        self.animate(to:arc*(-0.5), in:time*0.5, from:p as! CGPath)
    }
}

Upvotes: 4

Related Questions