How to draw an animated path with multiple colors in ios?
Asked Answered
T

3

8

I need to draw something like the following image in my iOS app, except that the arc may contain more colors:

Path to draw

I know how to draw it, but I'm looking for a way to animate the drawing of the path.

There is a similar question here, except that the circle is not animated. This is another question where it's explained how to animate a circle, and it work ok in my case, except that it doesn't handle multiple colors in the path.

How can accomplish this?

Triphthong answered 9/6, 2015 at 16:34 Comment(6)
I dunno the method how to do it the best way. But as temporary solution I'd create layers (i.e. CAShapeLayer) which number equals to your colors count. These layers would be just a segments (arc) of your circle. Than iterates over them and animates one by one. I'm sure that it would work and look smoothly without any problems. The only question that probably can worry U, is that maybe there is a more elegant way how to do thisTerminable
@Terminable Thanks for your comment. The problem that I see with your solution is that I don't know how to effectively synchronize the animationsTriphthong
It is not a big problem @Reynaldo Aguilar. Of course there is a delegate method (to check when animation completes), but as I remember it triggers with a delay, and does not fit well for a subsequent operations. BUT for a such case there is a grouped animation (CAAnimationGroup) where you simply define one by one animations and set start and end time for each of the separate animation. So you task looks quite easy: 1) know the exact number of the colors U gonna useTerminable
2) create the same number of CAShapeLayer "s (arcs ,of course they can have different length) and set "path" property, where path is UIBezierPath type 3) define CABasicAnimation for each layer with a correct animationWithKeyPath (@"strokeEnd"), then set "fromValue/toValue" properties (which is the values of your circle length , for example 0.5 - animates the half of the circle), set the animation duration and add this animation to the created layer. 4) Group these animations in a CAAnimationGroup and Voila. One of the possible example is MBProgressHUD, which draws the animatable circle.Terminable
Several examples: 1) cocoacontrols.com/controls/mmmaterialdesignspinner 2) cocoacontrols.com/controls/swiftspinner 3) github.com/jdg/MBProgressHUD 4) cocoacontrols.com/controls/bfradialwaveviewTerminable
@Terminable Ok, I get it now. Thanks for your links. I provided other solution that I figured by myself. I think that it may work in multiple cases.Triphthong
T
8

I found a general solution that work very well. Since there is no way for drawing a unique path of different colors, then I just draw, without animation, all the paths of different colors that compound the path that I want. After that, I drawn an unique path in the reverse direction that cover all those paths, and apply an animation to this path.

For example, in the case above, I draw both arcs with the following code:

class CircleView: UIView {

    let borderWidth: CGFloat = 20

    let startAngle = CGFloat(Double.pi)
    let middleAngle = CGFloat(Double.pi + Double.pi / 2)
    let endAngle = CGFloat(2 * Double.pi)
    var primaryColor = UIColor.red
    var secondaryColor = UIColor.blue
    var currentStrokeValue = CGFloat(0)

    override func draw(_ rect: CGRect) {
        let center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
        let radius = CGFloat(self.frame.width / 2 - borderWidth)
        let path1 = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: middleAngle, clockwise: true)
        let path2 = UIBezierPath(arcCenter: center, radius: radius, startAngle: middleAngle, endAngle: endAngle, clockwise: true)
        path1.lineWidth = borderWidth
        primaryColor.setStroke()
        path1.stroke()
        path2.lineWidth = borderWidth
        secondaryColor.setStroke()
        path2.stroke()
    }
}

After that, I get path path3 and then I add it to a layer that will be added to the view:

var path3 = UIBezierPath(arcCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: true)

Note in this path that it covers the previous two path in the reverse order, since its startAngle is equal to the value of endAngle, its endAngle is equal to startAngle and the clockwise property is set to true. This path is the one that I will go to animate.

For example, if I want to show the 40% of the whole (imaginary) path (the one composed by the paths of different colors), I translate that to show the 60% of my cover path path3. The way in which we can animate path3 can be found in the link provided in the question.

Triphthong answered 10/6, 2015 at 16:8 Comment(0)
M
0
import UIKit
import QuartzCore
import CoreGraphics

class ViewController: UIViewController,UIGestureRecognizerDelegate {
var btnview : UIButton!
var buttonCenter = CGPoint.zero
var firstlayerpoint = CGPoint.zero
var firstLayer = CAShapeLayer()
var secondLayer = CAShapeLayer()
var thirdLayer = CAShapeLayer()
var initialPosition = CGRect()
let label = UILabel()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var blueLabel: UILabel!
@IBOutlet weak var greenLabel: UILabel!
@IBOutlet weak var redLabel: UILabel!

override func viewDidLoad() {
    super.viewDidLoad()


        firstLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.blue, LineWidth: 20.0)
        firstLayer.strokeStart = 0.00
        firstLayer.strokeEnd = 0.33

        self.view.layer.addSublayer(firstLayer)


        secondLayer = self.createCircleWithBounds(bounds: CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.red, LineWidth: 20.0)
        secondLayer.strokeStart = 0.33
        secondLayer.strokeEnd = 0.66
        self.view.layer.addSublayer(secondLayer)


        thirdLayer = self.createCircleWithBounds(bounds:  CGRect(x:0, y:0, width:100,height:100), Position: self.view.center, StrokeColor: UIColor.green, LineWidth: 20.0)
        thirdLayer.strokeStart = 0.66
        thirdLayer.strokeEnd = 1.00
        self.view.layer.addSublayer(thirdLayer)

    btnview = UIButton(frame: CGRect(x: self.view.center.x - 20 , y: self.view.center.y - 20 , width: 40, height: 40))
    btnview.backgroundColor = UIColor.gray
    btnview.isUserInteractionEnabled = true
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panButton(panGesture:)))
    // panGesture.minimumNumberOfTouches = 1

    btnview.addGestureRecognizer(panGesture)

    self.view.addSubview(btnview)
    // Do any additional setup after loading the view, typically from a nib.
        nameLabel.isHidden = true
        blueLabel.isHidden = true
        greenLabel.isHidden = true
        redLabel.isHidden = true
}


func panButton(panGesture: UIPanGestureRecognizer) {

    //let translation = panGesture.translation(in: self.btnview)
    panGesture.view!.center =  btnview.center
    panGesture.setTranslation(CGPoint.zero, in: self.view)
    // var point = CGPoint.zero
    //  point = firstLayer.frame.size.center


    if panGesture.state == .began {


        label.isHidden = false
        buttonCenter = btnview.center // store old button center
    }
    else if panGesture.state == .ended || panGesture.state == .failed || panGesture.state == .cancelled {

        print(btnview.frame.origin.x)
        print(greenLabel.frame.origin.x)
        if btnview.frame.origin.x > greenLabel.frame.origin.x
        {
            //    lblflayer.isHidden = false
            //    lblsecondlayer.isHidden = true
            //    lblthirdlayer.isHidden = true
            nameLabel.isHidden = false
            nameLabel.text = "Blue"
            nameLabel.backgroundColor = UIColor.blue

        }
        else if btnview.frame.origin.x > blueLabel.frame.origin.x
        {
            // print(btnview.frame.origin.x)
            //  print(lblsecondlayer.frame.origin.x)
            //     lblsecondlayer.isHidden = false
            //   lblflayer.isHidden = true
            //    lblthirdlayer.isHidden = true
            nameLabel.isHidden = false
            nameLabel.text = "Red"
            nameLabel.backgroundColor = UIColor.red

        }

        else  if btnview.frame.origin.x > redLabel.frame.origin.x
        {
            print(btnview.frame.origin.x)
            print(redLabel.frame.origin.x)
            greenLabel.isHidden = true
            //  lblsecondlayer.isHidden = true
            //  lblthirdlayer.isHidden = false

            nameLabel.isHidden = false
            nameLabel.text = "Green"
            nameLabel.backgroundColor = UIColor.green
        }
        else
        {
            nameLabel.isHidden = true
            // lblflayer.isHidden = true
            // lblsecondlayer.isHidden = true
            //  lblthirdlayer.isHidden = true
        }

        btnview.center = buttonCenter // restore button center
    }
    else
    {
        let location = panGesture.location(in: view) // get pan location
        btnview.center = location // set button to where finger is
    }


}



func createCircleWithBounds(bounds: CGRect, Position position: CGPoint, StrokeColor color: UIColor, LineWidth lineWidth: CGFloat) -> CAShapeLayer {
    //let shapelayer = CAShapeLayer.layer
    let shapelayer = CAShapeLayer()
    shapelayer.strokeColor = color.cgColor
    shapelayer.fillColor = UIColor.clear.cgColor
    shapelayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.width / 2).cgPath
    shapelayer.bounds = bounds
    shapelayer.position = position
    shapelayer.lineCap = kCALineCapButt
    shapelayer.lineWidth = lineWidth
    return shapelayer
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


}
Miso answered 10/5, 2017 at 9:59 Comment(1)
Don't post a wall of code as an answer. For an answer post to be useful to others, add some explanations and provide shorter snippets for the key points of the solution.Mulct
A
0

@Reynaldo Aguilar 's answer works well. You can achieve the same thing by creating a mask layer and animating its frame. The benefit of this approach is that it works with any (and multi-colored) background. The downside to this approach is that it may not work if the line is not functional, since multiple points could be hidden/revealed at the same time (when you only want one of them to be) in that scenario.

For ease of use, you can subclass UIView and override its draw and init methods like so to add the lines.

INIT:

init(frame: CGRect, path: [UIBezierPath], strokeColor: [UIColor], fillColor: [UIColor], lineWidth: [CGFloat]) {
    // Initialize the view
    super.init(frame: frame)

    self.paths = path
    self.strokeColors = strokeColor
    self.fillColors = fillColor
    self.lineWidths = lineWidth

    self.backgroundColor = UIColor.clear // Background will always be clear by default
}

DRAW:

override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard paths.count == strokeColors.count && strokeColors.count == fillColors.count && fillColors.count == lineWidths.count else {
        print("ERROR: ARRAYS DON'T MATCH") // Stronger error handling recommended
        return
    }

    for psfl in 0..<paths.count {
        // Fill path if appropriate
        self.fillColors[psfl].setFill()
        self.paths[psfl].fill()

        self.strokeColors[psfl].setStroke()
        self.paths[psfl].lineWidth = self.lineWidths[psfl]
        self.paths[psfl].stroke()
    }
}

You can then have a function to animate the mask layer like so

ANIMATE:

func animate(startingRect: CGRect, duration: Double, animationKey: String) {
    // Create a path based on the starting rect
    let maskPath = UIBezierPath(rect: startingRect)
    // Create a path based on the final rect
    let finalPath = UIBezierPath(rect: self.frame)

    // Create a shapelayer for the animation block
    let maskLayer = CAShapeLayer()
    maskLayer.frame = startingRect

    // Add the mask layer to the custom view
    self.layer.mask = maskLayer

    // Animation block
    let animation = CABasicAnimation(keyPath: "path")
    animation.delegate = self // (Optionaly) set the delegate so we can remove the mask when the animation completes
    animation.fromValue = maskLayer.path
    animation.toValue = finalPath.cgPath
    animation.duration = duration
    maskLayer.add(animation, forKey: animationKey) // Add the animation to the mask layer

    // Necessary for the animation to work properly
    CATransaction.begin()
    CATransaction.setDisableActions(true)

    maskLayer.path = maskPath.cgPath
    CATransaction.commit()
}

With the subclass, implementation is easy. Initialize it, add it as a subview to where you want, and then call animate() when you want the animation to begin. You can also fiddle with other things, like the alphas during initialization and animation, if you want the drawings to be hidden to start.

Andradite answered 12/9, 2018 at 22:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.