Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using circular progress bar with masked image?

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

like image 626
Dominik Hadl Avatar asked May 11 '13 14:05

Dominik Hadl


2 Answers

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. ;)

like image 84
omz Avatar answered Nov 11 '22 21:11

omz


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"

like image 22
RealNmae Avatar answered Nov 11 '22 23:11

RealNmae