Draw animated rounded rect line with gradient color

73 Views Asked by At

So I'm trying to create a loading indicator on an element, basically it's a path that's expanding and contracting towards the end.

Loader

The hard part is that the path is a gradient color which I have no idea to while also animating the paths strokeStart and strokeEnd

Here is what I have so far without the gradient.

//
// Created by Tommy Sadiq Hinrichsen on 07/07/2021.
// Copyright (c) 2021 Dagrofa. All rights reserved.
//

import Foundation
import UIKit

@IBDesignable
class LoadingBorderView: UIView {

    private let strokeLayer = CAShapeLayer()

    var startPosition: UIRectEdge = .right {
        didSet { self.setNeedsDisplay() }
    }

    private var cornerRadius: CGFloat { return self.frame.height / 2 }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.configureView()
    }

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

    private func configureView() {
        self.strokeLayer.fillColor = nil
        self.strokeLayer.strokeColor = UIColor.black.cgColor
        self.strokeLayer.strokeEnd = 0
        self.strokeLayer.lineCap = .round

        self.layer.addSublayer(self.strokeLayer)

    }

    private func getRoundedPath() -> UIBezierPath {

        let path = UIBezierPath()

        let drawTop: (UIBezierPath) -> Void = { (path: UIBezierPath) in
            path.move(to: CGPoint(x: 0, y: self.cornerRadius))
            path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 180.cgfloat.toRadians(), endAngle: 270.cgfloat.toRadians(), clockwise: true)
            path.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: 0))
            path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: -90.cgfloat.toRadians(), endAngle: 0.cgfloat.toRadians(), clockwise: true)
        }

        let drawBottom: (UIBezierPath) -> Void = { (path: UIBezierPath) in
            path.move(to: CGPoint(x: self.frame.width, y: self.cornerRadius))
            path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 0.cgfloat.toRadians(), endAngle: 90.cgfloat.toRadians(), clockwise: true)
            path.addLine(to: CGPoint(x: self.cornerRadius, y: self.frame.height))
            path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 90.cgfloat.toRadians(), endAngle: 180.cgfloat.toRadians(), clockwise: true)
        }

        switch self.startPosition {
            case .right:
                drawBottom(path)
                drawTop(path)
            case .left:
                drawTop(path)
                drawBottom(path)
            default:
                fatalError("LoadingBorderView must start from left or right")
        }
        return path
    }

    func startAnimation(duration: TimeInterval = 2.0, delay: TimeInterval = 0.3) {

         let strokeStartAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart))
         strokeStartAnimation.beginTime = delay
         strokeStartAnimation.duration = duration
         strokeStartAnimation.fromValue = 0
         strokeStartAnimation.toValue = 1

        let strokeEndAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
        strokeEndAnimation.duration = duration
        strokeEndAnimation.fromValue = 0
        strokeEndAnimation.toValue = 1
        strokeEndAnimation.fillMode = .forwards

        let group = CAAnimationGroup()
        group.beginTime = CACurrentMediaTime()
        group.timingFunction = .easeInOutCubic
        group.duration = duration + delay
        group.repeatCount = .infinity
        group.animations = [strokeStartAnimation, strokeEndAnimation]

        self.strokeLayer.add(group, forKey: "strokeLayer")

    }

    func stopAnimation() {
        self.strokeLayer.removeAllAnimations()
    }

    override func draw(_ rect: CGRect) {
        self.strokeLayer.frame = self.bounds
        self.strokeLayer.path = self.getRoundedPath().cgPath
    }
}

public extension CAMediaTimingFunction {

    ///https://easings.net/#easeInOutCubic
    static var easeInOutCubic = CAMediaTimingFunction(controlPoints: 0.65, 0, 0.35, 1)

}
0

There are 0 best solutions below