Using circular progress bar with masked image?

时光毁灭记忆、已成空白 提交于 2019-12-21 01:40:07

问题


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


回答1:


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:

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




回答2:


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"



来源:https://stackoverflow.com/questions/16498390/using-circular-progress-bar-with-masked-image

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!