axelmukwena
axelmukwena

Reputation: 1059

Glitch at start of CABasicAnimation

I have a scaling/pulsing animation that is working on cgPaths in a for loop as below. This code works but only when you append the animatedLayers to the circleLayer path when the circleLayer has already been added to the subLayer and this creates a static circle (glitch-like) before the animation (DispatchQueue) starts...

...
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
...

enter image description here

Is it possible to add a CAShapeLayer with arguments to a subLayer? If not, any recommended alternative?

import UIKit
import Foundation

@IBDesignable
class AnimatedCircleView: UIView {

    // MARK: - Initializers

    var animatedLayers = [CAShapeLayer]()

    // MARK: - Methods

    override func draw(_ rect: CGRect) {

        // Animated circle
        for _ in 0...3 {
            let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
            startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
            let animatedLayer = CAShapeLayer()

            animatedLayer.path = animatedPath.cgPath
            animatedLayer.strokeColor = UIColor.black.cgColor
            animatedLayer.lineWidth = 0
            animatedLayer.fillColor = UIColor.gray.cgColor
            animatedLayer.lineCap = CAShapeLayerLineCap.round
            animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
            self.layer.addSublayer(animatedLayer)
            animatedLayers.append(animatedLayer)
        }

        // Dispatch animation for circle _ 0...3
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.animateCircle(index: 0)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.animateCircle(index: 1)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    self.animateCircle(index: 2)
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                        self.animateCircle(index: 3)
                    }
                }
            }
        }
    }


    func animateCircle(index: Int) {

        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = 1.8
        scaleAnimation.fromValue = 0
        scaleAnimation.toValue = 1
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        scaleAnimation.repeatCount = Float.infinity
        animatedLayers[index].add(scaleAnimation, forKey: "scale")

        let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        opacityAnimation.duration = 1.8
        opacityAnimation.fromValue = 0.7
        opacityAnimation.toValue = 0
        opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        opacityAnimation.repeatCount = Float.infinity
        animatedLayers[index].add(opacityAnimation, forKey: "opacity")
    }
}

Upvotes: 2

Views: 344

Answers (2)

Rob
Rob

Reputation: 437917

The key issue is that your animations start, with staggered delays, between in 0.1 and 1.0 seconds. Until that last animation starts, that layer is just sitting there, full sized and at 100% opacity.

Since you are animating the transform scale from 0 to 1, I’d suggest setting the starting transform to 0 (or changing the opacity to 0). Then you won’t see them sitting there until their respective animations start.

A few other observations:

  • The draw(_:) is not the right place to add layers, start animations, etc. This method may be called multiple times and should represent the view at a given point in time. I’d retire draw(_:) is it’s not the right place to start this and you don’t need this method at all.

  • You start your first animation after 0.1 seconds. Why not start it immediately?

  • You should handle frame adjustments by deferring the setting of the path and position properties until layoutSubviews.

Thus:

@IBDesignable
class AnimatedCircleView: UIView {
    private var animatedLayers = [CAShapeLayer]()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let path = UIBezierPath(arcCenter: .zero, radius: bounds.width / 2.3, startAngle: 0, endAngle: 2 * .pi, clockwise: true)

        for animatedLayer in animatedLayers {
            animatedLayer.path = path.cgPath
            animatedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
        }
    }
}

// MARK: - Methods

private extension AnimatedCircleView {
    func configure() {
        for _ in 0...3 {
            let animatedLayer = CAShapeLayer()
            animatedLayer.strokeColor = UIColor.black.cgColor
            animatedLayer.lineWidth = 0
            animatedLayer.fillColor = UIColor.gray.cgColor
            animatedLayer.lineCap = .round
            animatedLayer.transform = CATransform3DMakeScale(0, 0, 1)
            layer.addSublayer(animatedLayer)
            animatedLayers.append(animatedLayer)
        }

        self.animateCircle(index: 0)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.animateCircle(index: 1)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.animateCircle(index: 2)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    self.animateCircle(index: 3)
                }
            }
        }
    }

    func animateCircle(index: Int) {
        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = 1.8
        scaleAnimation.fromValue = 0
        scaleAnimation.toValue = 1
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        scaleAnimation.repeatCount = .greatestFiniteMagnitude
        animatedLayers[index].add(scaleAnimation, forKey: "scale")

        let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        opacityAnimation.duration = 1.8
        opacityAnimation.fromValue = 0.7
        opacityAnimation.toValue = 0
        opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        opacityAnimation.repeatCount = .greatestFiniteMagnitude
        animatedLayers[index].add(opacityAnimation, forKey: "opacity")
    }
}

Upvotes: 1

HalR
HalR

Reputation: 11083

Have you tried starting with the fillcolor as clear then turning it to grey at the start of the animation?

    // Animated circle
    for _ in 0...3 {
        let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
        startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
        let animatedLayer = CAShapeLayer()

        animatedLayer.path = animatedPath.cgPath
        animatedLayer.strokeColor = UIColor.black.cgColor
        animatedLayer.lineWidth = 0
        animatedLayer.fillColor = UIColor.clear.cgColor
        animatedLayer.lineCap = CAShapeLayerLineCap.round
        animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
        self.layer.addSublayer(animatedLayer)
        animatedLayers.append(animatedLayer)
    }

    // Dispatch animation for circle _ 0...3
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {

        self.animateCircle(index: 0)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            self.animateCircle(index: 1)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                self.animateCircle(index: 2)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                    self.animateCircle(index: 3)
                }
            }
        }
    }
}

Upvotes: 0

Related Questions