Using circular progress bar with masked image?
Asked Answered
J

2

6

I am working on a circular progress bar for custom game center achievements view and I have kind of "hit the wall". I am struggling with this for over two hours and still cannot get it to work.

The thing is, that I need a circular progress bar, where I would be able to set (at least) the track(fill) image. Because my progress fill is a rainbow like gradient and I am not able to achieve the same results using only code.

I was messing with CALayers, and drawRect method, however I wasn't successful. Could you please give me a little guidance?

Here are examples of the circular progress bars: https://github.com/donnellyk/KDGoalBar https://github.com/danielamitay/DACircularProgress

I just need the fill to be an masked image, depending on the progress. If you could even make it work that the progress would be animated, that would be really cool, but I don't require help with that :)

Thanks, Nick

Jigsaw answered 11/5, 2013 at 14:54 Comment(2)
So is it the masking or drawing a "rainbow" like gradient that is stopping you? Or is it putting the two pieces together?Desulphurize
I may have described it unclearly, sorry for that - I don't want to draw a gradient, I have a very nice gradient circular fill for the progress bar made in Photoshop, and I just need to mask it, so it is the fill of the progress bar. Do you know what I mean? :)Jigsaw
H
23

You basically just need to construct a path that defines the area to be filled (e.g. using CGPathAddArc), clip the graphics context to that path using CGContextClip and then just draw your image.

Here's an example of a drawRect: method you could use in a custom view:

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGFloat progress = 0.7f; //This would be a property of your view
    CGFloat innerRadiusRatio = 0.5f; //Adjust as needed

    //Construct the path:
    CGMutablePathRef path = CGPathCreateMutable();
    CGFloat startAngle = -M_PI_2;
    CGFloat endAngle = -M_PI_2 + MIN(1.0f, progress) * M_PI * 2;
    CGFloat outerRadius = CGRectGetWidth(self.bounds) * 0.5f - 1.0f;
    CGFloat innerRadius = outerRadius * innerRadiusRatio;
    CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    CGPathAddArc(path, NULL, center.x, center.y, innerRadius, startAngle, endAngle, false);
    CGPathAddArc(path, NULL, center.x, center.y, outerRadius, endAngle, startAngle, true);
    CGPathCloseSubpath(path);
    CGContextAddPath(ctx, path);
    CGPathRelease(path);

    //Draw the image, clipped to the path:
    CGContextSaveGState(ctx);
    CGContextClip(ctx);
    CGContextDrawImage(ctx, self.bounds, [[UIImage imageNamed:@"RadialProgressFill"] CGImage]);
    CGContextRestoreGState(ctx);
}

To keep it simple, I've hard-coded a few things – you obviously need to add a property for progress and call setNeedsDisplay in the setter. This also assumes that you have an image named RadialProgressFill in your project.

Here's an example of what that would roughly look like:

Screenshot

I hope you have a better-looking background image. ;)

Hellebore answered 14/5, 2013 at 17:23 Comment(3)
I hope there will be more and more people like you - this is much more than I expected! Thank you very much for your detailed answer - I have rewarded you :)Jigsaw
@DominikHadl How and where to cal this method - (void)drawRect:(CGRect)rectDonoghue
great stuff, would be nice to have it as a github projectVole
F
1

The solution that omz proposed worked laggy when I was trying to animate property, so I kept looking. Found great solution - to use Quartz Core framework and wrapper called CADisplayLink. "This class is specifically made for animations where “your data is extremely likely to change in every frame”. It attempts to send a message to a target every time something is redrawn to the screen" Source

And the library that work that way.

Edit: But there is more efficient solution. To animate mask which is applied on layer with the image. I don't know why I didn't do it from the beginning. CABasicAnimation works faster than any custom drawing. Here is my implementation:

class ProgressBar: UIView {
    var imageView:UIImageView!

    var maskLayer: CAShapeLayer!
    var currentValue: CGFloat = 1

    override init(frame: CGRect) {
        super.init(frame: frame)
        updateUI()
    }

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

    func updateUI(){
        makeGradient()
        setUpMask()
    }

    func animateCircle(strokeEnd: CGFloat) {
        let oldStrokeEnd = maskLayer.strokeEnd
        maskLayer.strokeEnd = strokeEnd

        //override strokeEnd implicit animation
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = 0.5
        animation.fromValue = oldStrokeEnd
        animation.toValue = strokeEnd
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        maskLayer.add(animation, forKey: "animateCircle")

        currentValue = strokeEnd
    }

    func setUpMask(){

        let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0),
                                      radius: (frame.size.width - 10)/2,
                                      startAngle: -CGFloat.pi/2,
                                      endAngle: CGFloat.pi*1.5,
                                      clockwise: true)

        maskLayer = CAShapeLayer()
        maskLayer.path = circlePath.cgPath
        maskLayer.fillColor = UIColor.clear.cgColor
        maskLayer.strokeColor = UIColor.red.cgColor
        maskLayer.lineWidth = 8.0;
        maskLayer.strokeEnd = currentValue
        maskLayer.lineCap = "round"
        imageView.layer.mask = maskLayer
    }

    func makeGradient(){
        imageView = UIImageView(image: UIImage(named: "gradientImage"))
        imageView.frame = bounds
        imageView.contentMode = .scaleAspectFill
        addSubview(imageView)
    }

}

By the way, if you are looking to become better at animations, I suggest a great book "IOS Core Animation: Advanced Techniques Book by Nick Lockwood"

Fredericksburg answered 23/11, 2016 at 14:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.